1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-28 08:48:48 +02:00

Merge pull request #4057 from vcmi/beta

Merge beta -> master
This commit is contained in:
Ivan Savenko 2024-05-30 14:59:19 +03:00 committed by GitHub
commit 9121fcdc97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
170 changed files with 4224 additions and 2152 deletions

View File

@ -20,11 +20,11 @@ jobs:
matrix:
include:
- platform: linux-qt6
os: ubuntu-22.04
os: ubuntu-24.04
test: 0
preset: linux-clang-test
- platform: linux
os: ubuntu-22.04
os: ubuntu-24.04
test: 1
preset: linux-gcc-test
- platform: linux
@ -124,7 +124,7 @@ jobs:
# also, running it on multiple presets is redundant and slightly increases already long CI built times
if: ${{ startsWith(matrix.preset, 'linux-clang-test') }}
run: |
pip3 install jstyleson
sudo apt install python3-jstyleson
python3 CI/linux-qt6/validate_json.py
- name: Dependencies
@ -201,8 +201,8 @@ jobs:
- name: Configure
run: |
if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC12=1; fi
cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC12:+-DCMAKE_C_COMPILER=gcc-12 -DCMAKE_CXX_COMPILER=g++-12}
if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC14=1; fi
cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC14:+-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14}
- name: Build
run: |

View File

@ -77,7 +77,6 @@ AIGateway::AIGateway()
destinationTeleport = ObjectInstanceID();
destinationTeleportPos = int3(-1);
nullkiller.reset(new Nullkiller());
announcedCheatingProblem = false;
}
AIGateway::~AIGateway()
@ -378,7 +377,7 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor &
nullkiller->memory->removeFromMemory(obj);
nullkiller->objectClusterizer->onObjectRemoved(obj->id);
if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed())
if(nullkiller->baseGraph && nullkiller->isObjectGraphAllowed())
{
nullkiller->baseGraph->removeObject(obj);
}
@ -829,13 +828,9 @@ void AIGateway::makeTurn()
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
setThreadName("AIGateway::makeTurn");
if(cb->getStartInfo()->extraOptionsInfo.cheatsAllowed)
cb->sendMessage("vcmieagles");
else
if(nullkiller->isOpenMap())
{
if(!announcedCheatingProblem)
cb->sendMessage("Nullkiller AI currently requires the ability to cheat in order to function correctly! Please enable!");
announcedCheatingProblem = true;
cb->sendMessage("vcmieagles");
}
retrieveVisitableObjs();

View File

@ -95,7 +95,7 @@ public:
std::unique_ptr<boost::thread> makingTurn;
private:
boost::mutex turnInterruptionMutex;
bool announcedCheatingProblem;
public:
ObjectInstanceID selectedObject;

View File

@ -381,7 +381,7 @@ void ObjectClusterizer::clusterizeObject(
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
#endif
if(ai->settings->isObjectGraphAllowed())
if(ai->isObjectGraphAllowed())
{
ai->pathfinder->calculateQuickPathsWithBlocker(pathCache, heroes, obj->visitablePos());
}

View File

@ -47,7 +47,11 @@ bool CaptureObjectsBehavior::operator==(const CaptureObjectsBehavior & other) co
&& vectorEquals(objectSubTypes, other.objectSubTypes);
}
Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath> & paths, const Nullkiller * nullkiller, const CGObjectInstance * objToVisit)
Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
const std::vector<AIPath> & paths,
const Nullkiller * nullkiller,
const CGObjectInstance * objToVisit,
bool force)
{
Goals::TGoalVec tasks;
@ -72,7 +76,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
continue;
}
if(objToVisit && !shouldVisit(nullkiller, path.targetHero, objToVisit))
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Hero %s should not visit obj %s", path.targetHero->getNameTranslated(), objToVisit->getObjectName());
@ -200,12 +204,12 @@ void CaptureObjectsBehavior::decomposeObjects(
logAi->trace("Checking object %s, %s", objToVisit->getObjectName(), objToVisit->visitablePos().toString());
#endif
nullkiller->pathfinder->calculatePathInfo(paths, objToVisit->visitablePos(), nullkiller->settings->isObjectGraphAllowed());
nullkiller->pathfinder->calculatePathInfo(paths, objToVisit->visitablePos(), nullkiller->isObjectGraphAllowed());
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Found %d paths", paths.size());
#endif
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit));
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects));
}
std::lock_guard<std::mutex> lock(sync);

View File

@ -68,7 +68,11 @@ namespace Goals
bool operator==(const CaptureObjectsBehavior & other) const override;
static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const Nullkiller * nullkiller, const CGObjectInstance * objToVisit = nullptr);
static Goals::TGoalVec getVisitGoals(
const std::vector<AIPath> & paths,
const Nullkiller * nullkiller,
const CGObjectInstance * objToVisit = nullptr,
bool force = false);
private:
bool objectMatchesFilter(const CGObjectInstance * obj) const;

View File

@ -42,7 +42,7 @@ Goals::TGoalVec ClusterBehavior::decompose(const Nullkiller * ai) const
Goals::TGoalVec ClusterBehavior::decomposeCluster(const Nullkiller * ai, std::shared_ptr<ObjectCluster> cluster) const
{
auto center = cluster->calculateCenter(ai->cb.get());
auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->settings->isObjectGraphAllowed());
auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->isObjectGraphAllowed());
auto blockerPos = cluster->blocker->visitablePos();
std::vector<AIPath> blockerPaths;

View File

@ -0,0 +1,110 @@
/*
* ExplorationBehavior.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 "ExplorationBehavior.h"
#include "../AIGateway.h"
#include "../AIUtility.h"
#include "../Goals/Invalid.h"
#include "../Goals/Composition.h"
#include "../Goals/ExecuteHeroChain.h"
#include "../Markers/ExplorationPoint.h"
#include "../Goals/CaptureObject.h"
#include "../Goals/ExploreNeighbourTile.h"
#include "../Helpers/ExplorationHelper.h"
namespace NKAI
{
using namespace Goals;
std::string ExplorationBehavior::toString() const
{
return "Explore";
}
Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
{
Goals::TGoalVec tasks;
for(auto obj : ai->memory->visitableObjs)
{
if(!vstd::contains(ai->memory->alreadyVisited, obj))
{
switch(obj->ID.num)
{
case Obj::REDWOOD_OBSERVATORY:
case Obj::PILLAR_OF_FIRE:
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
break;
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE:
auto tObj = dynamic_cast<const CGTeleport *>(obj);
if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
{
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
}
break;
}
}
else
{
switch(obj->ID.num)
{
case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE:
auto tObj = dynamic_cast<const CGTeleport *>(obj);
if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
break;
for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits)
{
if(!cb->getObj(exit))
{
// Always attempt to visit two-way teleports if one of channel exits is not visible
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
break;
}
}
break;
}
}
}
auto heroes = ai->cb->getHeroesInfo();
for(const CGHeroInstance * hero : heroes)
{
ExplorationHelper scanResult(hero, ai);
if(scanResult.scanSector(1))
{
tasks.push_back(scanResult.makeComposition());
continue;
}
if(scanResult.scanSector(15))
{
tasks.push_back(scanResult.makeComposition());
continue;
}
if(ai->getScanDepth() == ScanDepth::ALL_FULL)
{
if(scanResult.scanMap())
{
tasks.push_back(scanResult.makeComposition());
}
}
}
return tasks;
}
}

View File

@ -0,0 +1,38 @@
/*
* ExplorationBehavior.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 "lib/VCMI_Lib.h"
#include "../Goals/CGoal.h"
#include "../AIUtility.h"
namespace NKAI
{
namespace Goals
{
class ExplorationBehavior : public CGoal<ExplorationBehavior>
{
public:
ExplorationBehavior()
:CGoal(EXPLORATION_BEHAVIOR)
{
}
TGoalVec decompose(const Nullkiller * ai) const override;
std::string toString() const override;
bool operator==(const ExplorationBehavior & other) const override
{
return true;
}
};
}
}

View File

@ -69,7 +69,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString());
#endif
auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed());
auto paths = ai->pathfinder->getPathInfo(pos, ai->isObjectGraphAllowed());
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Gather army found %d paths", paths.size());
@ -231,7 +231,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
logAi->trace("Checking ways to upgrade army in town %s, %s", upgrader->getObjectName(), pos.toString());
#endif
auto paths = ai->pathfinder->getPathInfo(pos, ai->settings->isObjectGraphAllowed());
auto paths = ai->pathfinder->getPathInfo(pos, ai->isObjectGraphAllowed());
auto goals = CaptureObjectsBehavior::getVisitGoals(paths, ai);
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;

View File

@ -15,6 +15,8 @@ set(Nullkiller_SRCS
Pathfinding/Rules/AIMovementToDestinationRule.cpp
Pathfinding/Rules/AIPreviousNodeRule.cpp
Pathfinding/ObjectGraph.cpp
Pathfinding/GraphPaths.cpp
Pathfinding/ObjectGraphCalculator.cpp
AIUtility.cpp
Analyzers/ArmyManager.cpp
Analyzers/HeroManager.cpp
@ -38,10 +40,12 @@ set(Nullkiller_SRCS
Goals/ExchangeSwapTownHeroes.cpp
Goals/CompleteQuest.cpp
Goals/StayAtTown.cpp
Goals/ExploreNeighbourTile.cpp
Markers/ArmyUpgrade.cpp
Markers/HeroExchange.cpp
Markers/UnlockCluster.cpp
Markers/DefendTown.cpp
Markers/ExplorationPoint.cpp
Engine/Nullkiller.cpp
Engine/DeepDecomposer.cpp
Engine/PriorityEvaluator.cpp
@ -57,7 +61,9 @@ set(Nullkiller_SRCS
Behaviors/GatherArmyBehavior.cpp
Behaviors/ClusterBehavior.cpp
Behaviors/StayAtTownBehavior.cpp
Behaviors/ExplorationBehavior.cpp
Helpers/ArmyFormation.cpp
Helpers/ExplorationHelper.cpp
AIGateway.cpp
)
@ -80,6 +86,8 @@ set(Nullkiller_HEADERS
Pathfinding/Rules/AIMovementToDestinationRule.h
Pathfinding/Rules/AIPreviousNodeRule.h
Pathfinding/ObjectGraph.h
Pathfinding/GraphPaths.h
Pathfinding/ObjectGraphCalculator.h
AIUtility.h
pforeach.h
Analyzers/ArmyManager.h
@ -107,10 +115,12 @@ set(Nullkiller_HEADERS
Goals/CompleteQuest.h
Goals/Goals.h
Goals/StayAtTown.h
Goals/ExploreNeighbourTile.h
Markers/ArmyUpgrade.h
Markers/HeroExchange.h
Markers/UnlockCluster.h
Markers/DefendTown.h
Markers/ExplorationPoint.h
Engine/Nullkiller.h
Engine/DeepDecomposer.h
Engine/PriorityEvaluator.h
@ -126,7 +136,9 @@ set(Nullkiller_HEADERS
Behaviors/GatherArmyBehavior.h
Behaviors/ClusterBehavior.h
Behaviors/StayAtTownBehavior.h
Behaviors/ExplorationBehavior.h
Helpers/ArmyFormation.h
Helpers/ExplorationHelper.h
AIGateway.h
)

View File

@ -19,8 +19,11 @@
#include "../Behaviors/GatherArmyBehavior.h"
#include "../Behaviors/ClusterBehavior.h"
#include "../Behaviors/StayAtTownBehavior.h"
#include "../Behaviors/ExplorationBehavior.h"
#include "../Goals/Invalid.h"
#include "../Goals/Composition.h"
#include "../../../lib/CPlayerState.h"
#include "../../lib/StartInfo.h"
namespace NKAI
{
@ -35,13 +38,45 @@ Nullkiller::Nullkiller()
{
memory = std::make_unique<AIMemory>();
settings = std::make_unique<Settings>();
useObjectGraph = settings->isObjectGraphAllowed();
openMap = settings->isOpenMap() || useObjectGraph;
}
bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
{
if(!cb->getStartInfo()->extraOptionsInfo.cheatsAllowed)
{
return false;
}
const TeamState * team = cb->getPlayerTeam(playerID);
auto hasHumanInTeam = vstd::contains_if(team->players, [cb](PlayerColor teamMateID) -> bool
{
return cb->getPlayerState(teamMateID)->isHuman();
});
if(hasHumanInTeam)
{
return false;
}
return cb->getStartInfo()->difficulty >= 3;
}
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
{
this->cb = cb;
this->gateway = gateway;
this->playerID = gateway->playerID;
playerID = gateway->playerID;
if(openMap && !canUseOpenMap(cb, playerID))
{
useObjectGraph = false;
openMap = false;
}
baseGraph.reset();
@ -190,7 +225,7 @@ void Nullkiller::resetAiState()
useHeroChain = true;
objectClusterizer->reset();
if(!baseGraph && settings->isObjectGraphAllowed())
if(!baseGraph && isObjectGraphAllowed())
{
baseGraph = std::make_unique<ObjectGraph>();
baseGraph->updateGraph(this);
@ -237,12 +272,12 @@ void Nullkiller::updateAiState(int pass, bool fast)
cfg.useHeroChain = useHeroChain;
cfg.allowBypassObjects = true;
if(scanDepth == ScanDepth::SMALL || settings->isObjectGraphAllowed())
if(scanDepth == ScanDepth::SMALL || isObjectGraphAllowed())
{
cfg.mainTurnDistanceLimit = settings->getMainHeroTurnDistanceLimit();
}
if(scanDepth != ScanDepth::ALL_FULL || settings->isObjectGraphAllowed())
if(scanDepth != ScanDepth::ALL_FULL || isObjectGraphAllowed())
{
cfg.scoutTurnDistanceLimit =settings->getScoutHeroTurnDistanceLimit();
}
@ -251,7 +286,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
pathfinder->updatePaths(activeHeroes, cfg);
if(settings->isObjectGraphAllowed())
if(isObjectGraphAllowed())
{
pathfinder->updateGraphs(
activeHeroes,
@ -354,6 +389,9 @@ void Nullkiller::makeTurn()
decompose(bestTasks, sptr(GatherArmyBehavior()), MAX_DEPTH);
decompose(bestTasks, sptr(StayAtTownBehavior()), MAX_DEPTH);
if(!isOpenMap())
decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
{
decompose(bestTasks, sptr(StartupBehavior()), 1);

View File

@ -76,6 +76,8 @@ private:
TResources lockedResources;
bool useHeroChain;
AIGateway * gateway;
bool openMap;
bool useObjectGraph;
public:
static std::unique_ptr<ObjectGraph> baseGraph;
@ -116,6 +118,8 @@ public:
void lockResources(const TResources & res);
const TResources & getLockedResources() const { return lockedResources; }
ScanDepth getScanDepth() const { return scanDepth; }
bool isOpenMap() const { return openMap; }
bool isObjectGraphAllowed() const { return useObjectGraph; }
private:
void resetAiState();

View File

@ -735,6 +735,20 @@ public:
}
};
class ExplorePointEvaluator : public IEvaluationContextBuilder
{
public:
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::EXPLORATION_POINT)
return;
int tilesDiscovered = task->value;
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
}
};
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
{
public:
@ -1056,6 +1070,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
evaluationContextBuilders.push_back(std::make_shared<StayAtTownManaRecoveryEvaluator>());
evaluationContextBuilders.push_back(std::make_shared<ExplorePointEvaluator>());
}
EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const

View File

@ -28,8 +28,9 @@ namespace NKAI
scoutHeroTurnDistanceLimit(5),
maxGoldPressure(0.3f),
maxpass(10),
allowObjectGraph(false),
useTroopsFromGarrisons(false)
allowObjectGraph(true),
useTroopsFromGarrisons(false),
openMap(true)
{
JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
@ -63,6 +64,11 @@ namespace NKAI
allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
}
if(!node.Struct()["openMap"].isNull())
{
openMap = node.Struct()["openMap"].Bool();
}
if(!node.Struct()["useTroopsFromGarrisons"].isNull())
{
useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();

View File

@ -28,6 +28,7 @@ namespace NKAI
float maxGoldPressure;
bool allowObjectGraph;
bool useTroopsFromGarrisons;
bool openMap;
public:
Settings();
@ -39,5 +40,6 @@ namespace NKAI
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
bool isObjectGraphAllowed() const { return allowObjectGraph; }
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
bool isOpenMap() const { return openMap; }
};
}

View File

@ -73,7 +73,10 @@ namespace Goals
CAPTURE_OBJECT,
SAVE_RESOURCES,
STAY_AT_TOWN_BEHAVIOR,
STAY_AT_TOWN
STAY_AT_TOWN,
EXPLORATION_BEHAVIOR,
EXPLORATION_POINT,
EXPLORE_NEIGHBOUR_TILE
};
class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>

View File

@ -18,10 +18,11 @@ class AIGateway;
namespace Goals
{
template<typename T> class DLL_EXPORT CGoal : public AbstractGoal
template<typename T>
class DLL_EXPORT CGoal : public AbstractGoal
{
public:
CGoal<T>(EGoals goal = INVALID) : AbstractGoal(goal)
CGoal(EGoals goal = INVALID) : AbstractGoal(goal)
{
isAbstract = true;
value = 0;
@ -32,7 +33,7 @@ namespace Goals
town = nullptr;
}
CGoal<T> * clone() const override
CGoal * clone() const override
{
return new T(static_cast<T const &>(*this)); //casting enforces template instantiation
}
@ -70,15 +71,16 @@ namespace Goals
}
};
template<typename T> class DLL_EXPORT ElementarGoal : public CGoal<T>, public ITask
template<typename T>
class DLL_EXPORT ElementarGoal : public CGoal<T>, public ITask
{
public:
ElementarGoal<T>(EGoals goal = INVALID) : CGoal<T>(goal), ITask()
ElementarGoal(EGoals goal = INVALID) : CGoal<T>(goal), ITask()
{
AbstractGoal::isAbstract = false;
}
ElementarGoal<T>(const ElementarGoal<T> & other) : CGoal<T>(other), ITask(other)
ElementarGoal(const ElementarGoal<T> & other) : CGoal<T>(other), ITask(other)
{
}

View File

@ -148,7 +148,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
return;
}
}
else if(i > 0 && ai->nullkiller->settings->isObjectGraphAllowed())
else if(i > 0 && ai->nullkiller->isObjectGraphAllowed())
{
auto chainMask = i < chainPath.nodes.size() - 1 ? chainPath.nodes[i + 1].chainMask : node->chainMask;

View File

@ -0,0 +1,69 @@
/*
* ExploreNeighbourTile.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 "ExploreNeighbourTile.h"
#include "../AIGateway.h"
#include "../AIUtility.h"
#include "../Helpers/ExplorationHelper.h"
namespace NKAI
{
using namespace Goals;
bool ExploreNeighbourTile::operator==(const ExploreNeighbourTile & other) const
{
return false;
}
void ExploreNeighbourTile::accept(AIGateway * ai)
{
ExplorationHelper h(hero, ai->nullkiller.get());
for(int i = 0; i < tilesToExplore && hero->movementPointsRemaining() > 0; i++)
{
int3 pos = hero->visitablePos();
float value = 0;
int3 target = int3(-1);
foreach_neighbour(pos, [&](int3 tile)
{
auto pathInfo = ai->myCb->getPathsInfo(hero)->getPathInfo(tile);
if(pathInfo->turns > 0)
return;
if(pathInfo->accessible == EPathAccessibility::ACCESSIBLE)
{
float newValue = h.howManyTilesWillBeDiscovered(tile);
newValue /= std::min(0.1f, pathInfo->getCost());
if(newValue > value)
{
value = newValue;
target = tile;
}
}
});
if(!target.valid() || !ai->moveHeroToTile(target, hero))
{
return;
}
}
}
std::string ExploreNeighbourTile::toString() const
{
return "Explore neighbour tiles by " + hero->getNameTranslated();
}
}

View File

@ -0,0 +1,45 @@
/*
* ExploreNeighbourTile.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 "CGoal.h"
namespace NKAI
{
class AIGateway;
class FuzzyHelper;
namespace Goals
{
class DLL_EXPORT ExploreNeighbourTile : public ElementarGoal<ExploreNeighbourTile>
{
private:
int tilesToExplore;
public:
ExploreNeighbourTile(const CGHeroInstance * hero, int amount)
: ElementarGoal(Goals::EXPLORE_NEIGHBOUR_TILE)
{
tilesToExplore = amount;
sethero(hero);
}
bool operator==(const ExploreNeighbourTile & other) const override;
void accept(AIGateway * ai) override;
std::string toString() const override;
private:
//TSubgoal decomposeSingle() const override;
};
}
}

View File

@ -0,0 +1,235 @@
/*
* ExplorationHelper.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 "ExplorationHelper.h"
#include "../../../lib/mapObjects/CGTownInstance.h"
#include "../Engine/Nullkiller.h"
#include "../Goals/Invalid.h"
#include "../Goals/Composition.h"
#include "../Goals/ExecuteHeroChain.h"
#include "../Markers/ExplorationPoint.h"
#include "../../../lib/CPlayerState.h"
#include "../Behaviors/CaptureObjectsBehavior.h"
#include "../Goals/ExploreNeighbourTile.h"
namespace NKAI
{
using namespace Goals;
ExplorationHelper::ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai)
:ai(ai), cbp(ai->cb.get()), hero(hero)
{
ts = cbp->getPlayerTeam(ai->playerID);
sightRadius = hero->getSightRadius();
bestGoal = sptr(Goals::Invalid());
bestValue = 0;
bestTilesDiscovered = 0;
ourPos = hero->visitablePos();
allowDeadEndCancellation = true;
}
TSubgoal ExplorationHelper::makeComposition() const
{
Composition c;
c.addNext(ExplorationPoint(bestTile, bestTilesDiscovered));
c.addNextSequence({bestGoal, sptr(ExploreNeighbourTile(hero, 5))});
return sptr(c);
}
bool ExplorationHelper::scanSector(int scanRadius)
{
int3 tile = int3(0, 0, ourPos.z);
const auto & slice = (*(ts->fogOfWarMap))[ourPos.z];
for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++)
{
for(tile.y = ourPos.y - scanRadius; tile.y <= ourPos.y + scanRadius; tile.y++)
{
if(cbp->isInTheMap(tile) && slice[tile.x][tile.y])
{
scanTile(tile);
}
}
}
return !bestGoal->invalid();
}
bool ExplorationHelper::scanMap()
{
int3 mapSize = cbp->getMapSize();
int perimeter = 2 * sightRadius * (mapSize.x + mapSize.y);
std::vector<int3> from;
std::vector<int3> to;
from.reserve(perimeter);
to.reserve(perimeter);
foreach_tile_pos([&](const int3 & pos)
{
if((*(ts->fogOfWarMap))[pos.z][pos.x][pos.y])
{
bool hasInvisibleNeighbor = false;
foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour)
{
if(!(*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
{
hasInvisibleNeighbor = true;
}
});
if(hasInvisibleNeighbor)
from.push_back(pos);
}
});
logAi->debug("Exploration scan visible area perimeter for hero %s", hero->getNameTranslated());
for(const int3 & tile : from)
{
scanTile(tile);
}
if(!bestGoal->invalid())
{
return false;
}
allowDeadEndCancellation = false;
for(int i = 0; i < sightRadius; i++)
{
getVisibleNeighbours(from, to);
vstd::concatenate(from, to);
vstd::removeDuplicates(from);
}
logAi->debug("Exploration scan all possible tiles for hero %s", hero->getNameTranslated());
for(const int3 & tile : from)
{
scanTile(tile);
}
return !bestGoal->invalid();
}
void ExplorationHelper::scanTile(const int3 & tile)
{
if(tile == ourPos
|| !ai->cb->getTile(tile, false)
|| !ai->pathfinder->isTileAccessible(hero, tile)) //shouldn't happen, but it does
return;
int tilesDiscovered = howManyTilesWillBeDiscovered(tile);
if(!tilesDiscovered)
return;
auto paths = ai->pathfinder->getPathInfo(tile);
auto waysToVisit = CaptureObjectsBehavior::getVisitGoals(paths, ai, ai->cb->getTopObj(tile));
for(int i = 0; i != paths.size(); i++)
{
auto & path = paths[i];
auto goal = waysToVisit[i];
if(path.exchangeCount > 1 || path.targetHero != hero || path.movementCost() <= 0.0 || goal->invalid())
continue;
float ourValue = (float)tilesDiscovered * tilesDiscovered / path.movementCost();
if(ourValue > bestValue) //avoid costly checks of tiles that don't reveal much
{
auto obj = cb->getTopObj(tile);
// picking up resources does not yield any exploration at all.
// if it blocks the way to some explorable tile AIPathfinder will take care of it
if(obj && obj->isBlockedVisitable())
{
continue;
}
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
{
bestGoal = goal;
bestValue = ourValue;
bestTile = tile;
bestTilesDiscovered = tilesDiscovered;
}
}
}
}
void ExplorationHelper::getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const
{
for(const int3 & tile : tiles)
{
foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour)
{
if((*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
{
out.push_back(neighbour);
}
});
}
}
int ExplorationHelper::howManyTilesWillBeDiscovered(const int3 & pos) const
{
int ret = 0;
int3 npos = int3(0, 0, pos.z);
const auto & slice = (*(ts->fogOfWarMap))[pos.z];
for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++)
{
for(npos.y = pos.y - sightRadius; npos.y <= pos.y + sightRadius; npos.y++)
{
if(cbp->isInTheMap(npos)
&& pos.dist2d(npos) - 0.5 < sightRadius
&& !slice[npos.x][npos.y])
{
if(allowDeadEndCancellation
&& !hasReachableNeighbor(npos))
{
continue;
}
ret++;
}
}
}
return ret;
}
bool ExplorationHelper::hasReachableNeighbor(const int3 & pos) const
{
for(const int3 & dir : int3::getDirs())
{
int3 tile = pos + dir;
if(cbp->isInTheMap(tile))
{
auto isAccessible = ai->pathfinder->isTileAccessible(hero, tile);
if(isAccessible)
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,51 @@
/*
* ExplorationHelper.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 "../AIUtility.h"
#include "../../../lib/GameConstants.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/CBuildingHandler.h"
#include "../Goals/AbstractGoal.h"
namespace NKAI
{
class ExplorationHelper
{
private:
const CGHeroInstance * hero;
int sightRadius;
float bestValue;
Goals::TSubgoal bestGoal;
int3 bestTile;
int bestTilesDiscovered;
const Nullkiller * ai;
CCallback * cbp;
const TeamState * ts;
int3 ourPos;
bool allowDeadEndCancellation;
public:
ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai);
Goals::TSubgoal makeComposition() const;
bool scanSector(int scanRadius);
bool scanMap();
int howManyTilesWillBeDiscovered(const int3 & pos) const;
private:
void scanTile(const int3 & tile);
bool hasReachableNeighbor(const int3 & pos) const;
void getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const;
};
}

View File

@ -0,0 +1,32 @@
/*
* HeroExchange.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 "ExplorationPoint.h"
#include "../AIGateway.h"
#include "../Engine/Nullkiller.h"
#include "../AIUtility.h"
#include "../Analyzers/ArmyManager.h"
namespace NKAI
{
using namespace Goals;
bool ExplorationPoint::operator==(const ExplorationPoint & other) const
{
return false;
}
std::string ExplorationPoint::toString() const
{
return "Explore " +tile.toString() + " for " + std::to_string(value) + " tiles";
}
}

View File

@ -0,0 +1,35 @@
/*
* ExplorationPoint.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 "../Goals/CGoal.h"
#include "../Pathfinding/AINodeStorage.h"
namespace NKAI
{
namespace Goals
{
class DLL_EXPORT ExplorationPoint : public CGoal<ExplorationPoint>
{
public:
ExplorationPoint(int3 tile, int tilesToReviel)
: CGoal(Goals::EXPLORATION_POINT)
{
settile(tile);
setvalue(tilesToReviel);
}
bool operator==(const ExplorationPoint & other) const override;
std::string toString() const override;
};
}
}

View File

@ -12,6 +12,7 @@
#include "AINodeStorage.h"
#include "ObjectGraph.h"
#include "GraphPaths.h"
#include "../AIUtility.h"
namespace NKAI

View File

@ -0,0 +1,393 @@
/*
* GraphPaths.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 "GraphPaths.h"
#include "AIPathfinderConfig.h"
#include "../../../lib/CRandomGenerator.h"
#include "../../../CCallback.h"
#include "../../../lib/mapping/CMap.h"
#include "../Engine/Nullkiller.h"
#include "../../../lib/logging/VisualLogger.h"
#include "Actions/QuestAction.h"
#include "../pforeach.h"
#include "Actions/BoatActions.h"
namespace NKAI
{
bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const
{
return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost;
}
GraphPaths::GraphPaths()
: visualKey(""), graph(), pathNodes()
{
}
std::shared_ptr<SpecialAction> getCompositeAction(
const Nullkiller * ai,
std::shared_ptr<ISpecialActionFactory> linkActionFactory,
std::shared_ptr<SpecialAction> transitionAction)
{
if(!linkActionFactory)
return transitionAction;
auto linkAction = linkActionFactory->create(ai);
if(!transitionAction)
return linkAction;
std::vector<std::shared_ptr<const SpecialAction>> actionsArray = {
transitionAction,
linkAction
};
return std::make_shared<CompositeAction>(actionsArray);
}
void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth)
{
graph.copyFrom(*ai->baseGraph);
graph.connectHeroes(ai);
visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
pathNodes.clear();
GraphNodeComparer cmp(pathNodes);
GraphPathNode::TFibHeap pq(cmp);
pathNodes[targetHero->visitablePos()][GrapthPathNodeType::NORMAL].cost = 0;
pq.emplace(GraphPathNodePointer(targetHero->visitablePos(), GrapthPathNodeType::NORMAL));
while(!pq.empty())
{
GraphPathNodePointer pos = pq.top();
pq.pop();
auto & node = getOrCreateNode(pos);
std::shared_ptr<SpecialAction> transitionAction;
if(node.obj)
{
if(node.obj->ID == Obj::QUEST_GUARD
|| node.obj->ID == Obj::BORDERGUARD
|| node.obj->ID == Obj::BORDER_GATE)
{
auto questObj = dynamic_cast<const IQuestObject *>(node.obj);
auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord);
if(node.obj->ID == Obj::QUEST_GUARD
&& questObj->quest->mission == Rewardable::Limiter{}
&& questObj->quest->killTarget == ObjectInstanceID::NONE)
{
continue;
}
auto questAction = std::make_shared<AIPathfinding::QuestAction>(questInfo);
if(!questAction->canAct(ai, targetHero))
{
transitionAction = questAction;
}
}
}
node.isInQueue = false;
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq, scanDepth](int3 target, const ObjectLink & o)
{
auto compositeAction = getCompositeAction(ai, o.specialAction, transitionAction);
auto targetNodeType = o.danger || compositeAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
auto targetPointer = GraphPathNodePointer(target, targetNodeType);
auto & targetNode = getOrCreateNode(targetPointer);
if(targetNode.tryUpdate(pos, node, o))
{
if(targetNode.cost > scanDepth)
{
return;
}
targetNode.specialAction = compositeAction;
auto targetGraphNode = graph.getNode(target);
if(targetGraphNode.objID.hasValue())
{
targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false);
if(targetNode.obj && targetNode.obj->ID == Obj::HERO)
return;
}
if(targetNode.isInQueue)
{
pq.increase(targetNode.handle);
}
else
{
targetNode.handle = pq.emplace(targetPointer);
targetNode.isInQueue = true;
}
}
});
}
}
void GraphPaths::dumpToLog() const
{
logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder)
{
for(auto & tile : pathNodes)
{
for(auto & node : tile.second)
{
if(!node.previous.valid())
continue;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace(
"%s -> %s: %f !%d",
node.previous.coord.toString(),
tile.first.toString(),
node.cost,
node.danger);
}
logBuilder.addLine(node.previous.coord, tile.first);
}
}
});
}
bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link)
{
auto newCost = prev.cost + link.cost;
if(newCost < cost)
{
previous = pos;
danger = prev.danger + link.danger;
cost = newCost;
return true;
}
return false;
}
void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
{
auto nodes = pathNodes.find(tile);
if(nodes == pathNodes.end())
return;
for(auto & node : nodes->second)
{
if(!node.reachable())
continue;
std::vector<GraphPathNodePointer> tilesToPass;
uint64_t danger = node.danger;
float cost = node.cost;
bool allowBattle = false;
auto current = GraphPathNodePointer(nodes->first, node.nodeType);
while(true)
{
auto currentTile = pathNodes.find(current.coord);
if(currentTile == pathNodes.end())
break;
auto currentNode = currentTile->second[current.nodeType];
if(!currentNode.previous.valid())
break;
allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
vstd::amax(danger, currentNode.danger);
vstd::amax(cost, currentNode.cost);
tilesToPass.push_back(current);
if(currentNode.cost < 2.0f)
break;
current = currentNode.previous;
}
if(tilesToPass.empty())
continue;
auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
for(auto & path : entryPaths)
{
if(path.targetHero != hero)
continue;
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
{
AIPathNodeInfo n;
n.coord = graphTile->coord;
n.cost = cost;
n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
n.danger = danger;
n.targetHero = hero;
n.parentIndex = -1;
n.specialAction = getNode(*graphTile).specialAction;
if(n.specialAction)
{
n.actionIsBlocked = !n.specialAction->canAct(ai, n);
}
for(auto & node : path.nodes)
{
node.parentIndex++;
}
path.nodes.insert(path.nodes.begin(), n);
}
path.armyLoss += ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
paths.push_back(path);
}
}
}
void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
{
auto nodes = pathNodes.find(tile);
if(nodes == pathNodes.end())
return;
for(auto & targetNode : nodes->second)
{
if(!targetNode.reachable())
continue;
std::vector<GraphPathNodePointer> tilesToPass;
uint64_t danger = targetNode.danger;
float cost = targetNode.cost;
bool allowBattle = false;
auto current = GraphPathNodePointer(nodes->first, targetNode.nodeType);
while(true)
{
auto currentTile = pathNodes.find(current.coord);
if(currentTile == pathNodes.end())
break;
auto currentNode = currentTile->second[current.nodeType];
allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
vstd::amax(danger, currentNode.danger);
vstd::amax(cost, currentNode.cost);
tilesToPass.push_back(current);
if(currentNode.cost < 2.0f)
break;
current = currentNode.previous;
}
if(tilesToPass.empty())
continue;
auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
for(auto & entryPath : entryPaths)
{
if(entryPath.targetHero != hero)
continue;
auto & path = paths.emplace_back();
path.targetHero = entryPath.targetHero;
path.heroArmy = entryPath.heroArmy;
path.exchangeCount = entryPath.exchangeCount;
path.armyLoss = entryPath.armyLoss + ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
AIPathNodeInfo n;
n.targetHero = hero;
n.parentIndex = -1;
// final node
n.coord = tile;
n.cost = targetNode.cost;
n.danger = targetNode.danger;
n.parentIndex = path.nodes.size();
path.nodes.push_back(n);
for(auto entryNode = entryPath.nodes.rbegin(); entryNode != entryPath.nodes.rend(); entryNode++)
{
auto blocker = ai->objectClusterizer->getBlocker(*entryNode);
if(blocker)
{
// blocker node
path.nodes.push_back(*entryNode);
path.nodes.back().parentIndex = path.nodes.size() - 1;
break;
}
}
if(path.nodes.size() > 1)
continue;
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
{
auto & node = getNode(*graphTile);
n.coord = graphTile->coord;
n.cost = node.cost;
n.turns = static_cast<ui8>(node.cost);
n.danger = node.danger;
n.specialAction = node.specialAction;
n.parentIndex = path.nodes.size();
if(n.specialAction)
{
n.actionIsBlocked = !n.specialAction->canAct(ai, n);
}
auto blocker = ai->objectClusterizer->getBlocker(n);
if(!blocker)
continue;
// blocker node
path.nodes.push_back(n);
break;
}
}
}
}
}

View File

@ -0,0 +1,118 @@
/*
* GraphPaths.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 "ObjectGraph.h"
namespace NKAI
{
class Nullkiller;
struct GraphPathNode;
enum GrapthPathNodeType
{
NORMAL,
BATTLE,
LAST
};
struct GraphPathNodePointer
{
int3 coord = int3(-1);
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
GraphPathNodePointer() = default;
GraphPathNodePointer(int3 coord, GrapthPathNodeType type)
:coord(coord), nodeType(type)
{ }
bool valid() const
{
return coord.valid();
}
};
using GraphNodeStorage = std::unordered_map<int3, GraphPathNode[GrapthPathNodeType::LAST]>;
class GraphNodeComparer
{
const GraphNodeStorage & pathNodes;
public:
GraphNodeComparer(const GraphNodeStorage & pathNodes)
:pathNodes(pathNodes)
{
}
bool operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const;
};
struct GraphPathNode
{
const float BAD_COST = 100000;
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
GraphPathNodePointer previous;
float cost = BAD_COST;
uint64_t danger = 0;
const CGObjectInstance * obj = nullptr;
std::shared_ptr<SpecialAction> specialAction;
using TFibHeap = boost::heap::fibonacci_heap<GraphPathNodePointer, boost::heap::compare<GraphNodeComparer>>;
TFibHeap::handle_type handle;
bool isInQueue = false;
bool reachable() const
{
return cost < BAD_COST;
}
bool tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link);
};
class GraphPaths
{
ObjectGraph graph;
GraphNodeStorage pathNodes;
std::string visualKey;
public:
GraphPaths();
void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth);
void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
void quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
void dumpToLog() const;
private:
GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos)
{
auto & node = pathNodes[pos.coord][pos.nodeType];
node.nodeType = pos.nodeType;
return node;
}
const GraphPathNode & getNode(const GraphPathNodePointer & pos) const
{
auto & node = pathNodes.at(pos.coord)[pos.nodeType];
return node;
}
};
}

View File

@ -9,6 +9,7 @@
*/
#include "StdInc.h"
#include "ObjectGraph.h"
#include "ObjectGraphCalculator.h"
#include "AIPathfinderConfig.h"
#include "../../../lib/CRandomGenerator.h"
#include "../../../CCallback.h"
@ -22,392 +23,6 @@
namespace NKAI
{
struct ConnectionCostInfo
{
float totalCost = 0;
float avg = 0;
int connectionsCount = 0;
};
class ObjectGraphCalculator
{
private:
ObjectGraph * target;
const Nullkiller * ai;
std::mutex syncLock;
std::map<const CGHeroInstance *, HeroRole> actors;
std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
std::vector<std::unique_ptr<CGBoat>> temporaryBoats;
std::vector<std::unique_ptr<CGHeroInstance>> temporaryActorHeroes;
public:
ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
:ai(ai), target(target), syncLock()
{
}
void setGraphObjects()
{
for(auto obj : ai->memory->visitableObjs)
{
if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
{
addObjectActor(obj);
}
}
for(auto town : ai->cb->getTownsInfo())
{
addObjectActor(town);
}
}
void calculateConnections()
{
updatePaths();
std::vector<AIPath> pathCache;
foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
{
calculateConnections(pos, pathCache);
});
removeExtraConnections();
}
float getNeighborConnectionsCost(const int3 & pos, std::vector<AIPath> & pathCache)
{
float neighborCost = std::numeric_limits<float>::max();
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Checking junction %s", pos.toString());
}
foreach_neighbour(
ai->cb.get(),
pos,
[this, &neighborCost, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
ai->pathfinder->calculatePathInfo(pathCache, neighbor);
auto costTotal = this->getConnectionsCost(pathCache);
if(costTotal.connectionsCount > 2 && costTotal.avg < neighborCost)
{
neighborCost = costTotal.avg;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Better node found at %s", neighbor.toString());
}
}
});
return neighborCost;
}
void addMinimalDistanceJunctions()
{
tbb::concurrent_unordered_set<int3, std::hash<int3>> junctions;
pforeachTilePaths(ai->cb->getMapSize(), ai, [this, &junctions](const int3 & pos, std::vector<AIPath> & paths)
{
if(target->hasNodeAt(pos))
return;
if(ai->cb->getGuardingCreaturePosition(pos).valid())
return;
ConnectionCostInfo currentCost = getConnectionsCost(paths);
if(currentCost.connectionsCount <= 2)
return;
float neighborCost = getNeighborConnectionsCost(pos, paths);
if(currentCost.avg < neighborCost)
{
junctions.insert(pos);
}
});
for(auto pos : junctions)
{
addJunctionActor(pos);
}
}
private:
void updatePaths()
{
PathfinderSettings ps;
ps.mainTurnDistanceLimit = 5;
ps.scoutTurnDistanceLimit = 1;
ps.allowBypassObjects = false;
ai->pathfinder->updatePaths(actors, ps);
}
void calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache)
{
if(target->hasNodeAt(pos))
{
foreach_neighbour(
ai->cb.get(),
pos,
[this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
if(target->hasNodeAt(neighbor))
{
ai->pathfinder->calculatePathInfo(pathCache, neighbor);
for(auto & path : pathCache)
{
if(pos == path.targetHero->visitablePos())
{
target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger());
}
}
}
});
auto obj = ai->cb->getTopObj(pos);
if((obj && obj->ID == Obj::BOAT) || target->isVirtualBoat(pos))
{
ai->pathfinder->calculatePathInfo(pathCache, pos);
for(AIPath & path : pathCache)
{
auto from = path.targetHero->visitablePos();
auto fromObj = actorObjectMap[path.targetHero];
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos, path.targetHero, true);
auto updated = target->tryAddConnection(
from,
pos,
path.movementCost(),
danger);
if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
{
logAi->trace(
"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
fromObj ? fromObj->getObjectName() : "J", from.toString(),
"Boat", pos.toString(),
pos.toString(),
path.movementCost());
}
}
}
return;
}
auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
ai->pathfinder->calculatePathInfo(pathCache, pos);
for(AIPath & path1 : pathCache)
{
for(AIPath & path2 : pathCache)
{
if(path1.targetHero == path2.targetHero)
continue;
auto pos1 = path1.targetHero->visitablePos();
auto pos2 = path2.targetHero->visitablePos();
if(guardPos.valid() && guardPos != pos1 && guardPos != pos2)
continue;
auto obj1 = actorObjectMap[path1.targetHero];
auto obj2 = actorObjectMap[path2.targetHero];
auto tile1 = cb->getTile(pos1);
auto tile2 = cb->getTile(pos2);
if(tile2->isWater() && !tile1->isWater())
{
if(!cb->getTile(pos)->isWater())
continue;
auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
if(!startingObjIsBoat)
continue;
}
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
auto updated = target->tryAddConnection(
pos1,
pos2,
path1.movementCost() + path2.movementCost(),
danger);
if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
{
logAi->trace(
"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
obj1 ? obj1->getObjectName() : "J", pos1.toString(),
obj2 ? obj2->getObjectName() : "J", pos2.toString(),
pos.toString(),
path1.movementCost() + path2.movementCost());
}
}
}
}
bool isExtraConnection(float direct, float side1, float side2) const
{
float sideRatio = (side1 + side2) / direct;
return sideRatio < 1.25f && direct > side1 && direct > side2;
}
void removeExtraConnections()
{
std::vector<std::pair<int3, int3>> connectionsToRemove;
for(auto & actor : temporaryActorHeroes)
{
auto pos = actor->visitablePos();
auto & currentNode = target->getNode(pos);
target->iterateConnections(pos, [this, &pos, &connectionsToRemove, &currentNode](int3 n1, ObjectLink o1)
{
target->iterateConnections(n1, [&pos, &o1, &currentNode, &connectionsToRemove, this](int3 n2, ObjectLink o2)
{
auto direct = currentNode.connections.find(n2);
if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost))
{
connectionsToRemove.push_back({pos, n2});
}
});
});
}
vstd::removeDuplicates(connectionsToRemove);
for(auto & c : connectionsToRemove)
{
target->removeConnection(c.first, c.second);
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString());
}
}
}
void addObjectActor(const CGObjectInstance * obj)
{
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(obj->cb)).get();
CRandomGenerator rng;
auto visitablePos = obj->visitablePos();
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
if(cb->getTile(visitablePos)->isWater())
{
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = obj;
actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::BOAT ? HeroRole::MAIN : HeroRole::SCOUT;
target->addObject(obj);
auto shipyard = dynamic_cast<const IShipyard *>(obj);
if(shipyard && shipyard->bestLocation().valid())
{
int3 virtualBoat = shipyard->bestLocation();
addJunctionActor(virtualBoat, true);
target->addVirtualBoat(virtualBoat, obj);
}
}
void addJunctionActor(const int3 & visitablePos, bool isVirtualBoat = false)
{
std::lock_guard<std::mutex> lock(syncLock);
auto internalCb = temporaryActorHeroes.front()->cb;
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
CRandomGenerator rng;
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
{
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = nullptr;
actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
target->registerJunction(visitablePos);
}
ConnectionCostInfo getConnectionsCost(std::vector<AIPath> & paths) const
{
std::map<int3, float> costs;
for(auto & path : paths)
{
auto fromPos = path.targetHero->visitablePos();
auto cost = costs.find(fromPos);
if(cost == costs.end())
{
costs.emplace(fromPos, path.movementCost());
}
else
{
if(path.movementCost() < cost->second)
{
costs[fromPos] = path.movementCost();
}
}
}
ConnectionCostInfo result;
for(auto & cost : costs)
{
result.totalCost += cost.second;
result.connectionsCount++;
}
if(result.connectionsCount)
{
result.avg = result.totalCost / result.connectionsCount;
}
return result;
}
};
bool ObjectGraph::tryAddConnection(
const int3 & from,
const int3 & to,
@ -538,372 +153,4 @@ void ObjectGraph::dumpToLog(std::string visualKey) const
});
}
bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const
{
return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost;
}
GraphPaths::GraphPaths()
: visualKey(""), graph(), pathNodes()
{
}
std::shared_ptr<SpecialAction> getCompositeAction(
const Nullkiller * ai,
std::shared_ptr<ISpecialActionFactory> linkActionFactory,
std::shared_ptr<SpecialAction> transitionAction)
{
if(!linkActionFactory)
return transitionAction;
auto linkAction = linkActionFactory->create(ai);
if(!transitionAction)
return linkAction;
std::vector<std::shared_ptr<const SpecialAction>> actionsArray = {
transitionAction,
linkAction
};
return std::make_shared<CompositeAction>(actionsArray);
}
void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth)
{
graph.copyFrom(*ai->baseGraph);
graph.connectHeroes(ai);
visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
pathNodes.clear();
GraphNodeComparer cmp(pathNodes);
GraphPathNode::TFibHeap pq(cmp);
pathNodes[targetHero->visitablePos()][GrapthPathNodeType::NORMAL].cost = 0;
pq.emplace(GraphPathNodePointer(targetHero->visitablePos(), GrapthPathNodeType::NORMAL));
while(!pq.empty())
{
GraphPathNodePointer pos = pq.top();
pq.pop();
auto & node = getOrCreateNode(pos);
std::shared_ptr<SpecialAction> transitionAction;
if(node.obj)
{
if(node.obj->ID == Obj::QUEST_GUARD
|| node.obj->ID == Obj::BORDERGUARD
|| node.obj->ID == Obj::BORDER_GATE)
{
auto questObj = dynamic_cast<const IQuestObject *>(node.obj);
auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord);
if(node.obj->ID == Obj::QUEST_GUARD
&& questObj->quest->mission == Rewardable::Limiter{}
&& questObj->quest->killTarget == ObjectInstanceID::NONE)
{
continue;
}
auto questAction = std::make_shared<AIPathfinding::QuestAction>(questInfo);
if(!questAction->canAct(ai, targetHero))
{
transitionAction = questAction;
}
}
}
node.isInQueue = false;
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq, scanDepth](int3 target, const ObjectLink & o)
{
auto compositeAction = getCompositeAction(ai, o.specialAction, transitionAction);
auto targetNodeType = o.danger || compositeAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
auto targetPointer = GraphPathNodePointer(target, targetNodeType);
auto & targetNode = getOrCreateNode(targetPointer);
if(targetNode.tryUpdate(pos, node, o))
{
if(targetNode.cost > scanDepth)
{
return;
}
targetNode.specialAction = compositeAction;
auto targetGraphNode = graph.getNode(target);
if(targetGraphNode.objID.hasValue())
{
targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false);
if(targetNode.obj && targetNode.obj->ID == Obj::HERO)
return;
}
if(targetNode.isInQueue)
{
pq.increase(targetNode.handle);
}
else
{
targetNode.handle = pq.emplace(targetPointer);
targetNode.isInQueue = true;
}
}
});
}
}
void GraphPaths::dumpToLog() const
{
logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder)
{
for(auto & tile : pathNodes)
{
for(auto & node : tile.second)
{
if(!node.previous.valid())
continue;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace(
"%s -> %s: %f !%d",
node.previous.coord.toString(),
tile.first.toString(),
node.cost,
node.danger);
}
logBuilder.addLine(node.previous.coord, tile.first);
}
}
});
}
bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link)
{
auto newCost = prev.cost + link.cost;
if(newCost < cost)
{
previous = pos;
danger = prev.danger + link.danger;
cost = newCost;
return true;
}
return false;
}
void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
{
auto nodes = pathNodes.find(tile);
if(nodes == pathNodes.end())
return;
for(auto & node : nodes->second)
{
if(!node.reachable())
continue;
std::vector<GraphPathNodePointer> tilesToPass;
uint64_t danger = node.danger;
float cost = node.cost;
bool allowBattle = false;
auto current = GraphPathNodePointer(nodes->first, node.nodeType);
while(true)
{
auto currentTile = pathNodes.find(current.coord);
if(currentTile == pathNodes.end())
break;
auto currentNode = currentTile->second[current.nodeType];
if(!currentNode.previous.valid())
break;
allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
vstd::amax(danger, currentNode.danger);
vstd::amax(cost, currentNode.cost);
tilesToPass.push_back(current);
if(currentNode.cost < 2.0f)
break;
current = currentNode.previous;
}
if(tilesToPass.empty())
continue;
auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
for(auto & path : entryPaths)
{
if(path.targetHero != hero)
continue;
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
{
AIPathNodeInfo n;
n.coord = graphTile->coord;
n.cost = cost;
n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
n.danger = danger;
n.targetHero = hero;
n.parentIndex = -1;
n.specialAction = getNode(*graphTile).specialAction;
if(n.specialAction)
{
n.actionIsBlocked = !n.specialAction->canAct(ai, n);
}
for(auto & node : path.nodes)
{
node.parentIndex++;
}
path.nodes.insert(path.nodes.begin(), n);
}
path.armyLoss += ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
paths.push_back(path);
}
}
}
void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
{
auto nodes = pathNodes.find(tile);
if(nodes == pathNodes.end())
return;
for(auto & targetNode : nodes->second)
{
if(!targetNode.reachable())
continue;
std::vector<GraphPathNodePointer> tilesToPass;
uint64_t danger = targetNode.danger;
float cost = targetNode.cost;
bool allowBattle = false;
auto current = GraphPathNodePointer(nodes->first, targetNode.nodeType);
while(true)
{
auto currentTile = pathNodes.find(current.coord);
if(currentTile == pathNodes.end())
break;
auto currentNode = currentTile->second[current.nodeType];
allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
vstd::amax(danger, currentNode.danger);
vstd::amax(cost, currentNode.cost);
tilesToPass.push_back(current);
if(currentNode.cost < 2.0f)
break;
current = currentNode.previous;
}
if(tilesToPass.empty())
continue;
auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
for(auto & entryPath : entryPaths)
{
if(entryPath.targetHero != hero)
continue;
auto & path = paths.emplace_back();
path.targetHero = entryPath.targetHero;
path.heroArmy = entryPath.heroArmy;
path.exchangeCount = entryPath.exchangeCount;
path.armyLoss = entryPath.armyLoss + ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
AIPathNodeInfo n;
n.targetHero = hero;
n.parentIndex = -1;
// final node
n.coord = tile;
n.cost = targetNode.cost;
n.danger = targetNode.danger;
n.parentIndex = path.nodes.size();
path.nodes.push_back(n);
for(auto entryNode = entryPath.nodes.rbegin(); entryNode != entryPath.nodes.rend(); entryNode++)
{
auto blocker = ai->objectClusterizer->getBlocker(*entryNode);
if(blocker)
{
// blocker node
path.nodes.push_back(*entryNode);
path.nodes.back().parentIndex = path.nodes.size() - 1;
break;
}
}
if(path.nodes.size() > 1)
continue;
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
{
auto & node = getNode(*graphTile);
n.coord = graphTile->coord;
n.cost = node.cost;
n.turns = static_cast<ui8>(node.cost);
n.danger = node.danger;
n.specialAction = node.specialAction;
n.parentIndex = path.nodes.size();
if(n.specialAction)
{
n.actionIsBlocked = !n.specialAction->canAct(ai, n);
}
auto blocker = ai->objectClusterizer->getBlocker(n);
if(!blocker)
continue;
// blocker node
path.nodes.push_back(n);
break;
}
}
}
}
}

View File

@ -112,102 +112,4 @@ public:
}
};
struct GraphPathNode;
enum GrapthPathNodeType
{
NORMAL,
BATTLE,
LAST
};
struct GraphPathNodePointer
{
int3 coord = int3(-1);
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
GraphPathNodePointer() = default;
GraphPathNodePointer(int3 coord, GrapthPathNodeType type)
:coord(coord), nodeType(type)
{ }
bool valid() const
{
return coord.valid();
}
};
typedef std::unordered_map<int3, GraphPathNode[GrapthPathNodeType::LAST]> GraphNodeStorage;
class GraphNodeComparer
{
const GraphNodeStorage & pathNodes;
public:
GraphNodeComparer(const GraphNodeStorage & pathNodes)
:pathNodes(pathNodes)
{
}
bool operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const;
};
struct GraphPathNode
{
const float BAD_COST = 100000;
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
GraphPathNodePointer previous;
float cost = BAD_COST;
uint64_t danger = 0;
const CGObjectInstance * obj = nullptr;
std::shared_ptr<SpecialAction> specialAction;
using TFibHeap = boost::heap::fibonacci_heap<GraphPathNodePointer, boost::heap::compare<GraphNodeComparer>>;
TFibHeap::handle_type handle;
bool isInQueue = false;
bool reachable() const
{
return cost < BAD_COST;
}
bool tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link);
};
class GraphPaths
{
ObjectGraph graph;
GraphNodeStorage pathNodes;
std::string visualKey;
public:
GraphPaths();
void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai, uint8_t scanDepth);
void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
void quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
void dumpToLog() const;
private:
GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos)
{
auto & node = pathNodes[pos.coord][pos.nodeType];
node.nodeType = pos.nodeType;
return node;
}
const GraphPathNode & getNode(const GraphPathNodePointer & pos) const
{
auto & node = pathNodes.at(pos.coord)[pos.nodeType];
return node;
}
};
}

View File

@ -0,0 +1,387 @@
/*
* ObjectGraphCalculator.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 "ObjectGraphCalculator.h"
#include "AIPathfinderConfig.h"
#include "../../../lib/CRandomGenerator.h"
#include "../../../CCallback.h"
#include "../../../lib/mapping/CMap.h"
#include "../Engine/Nullkiller.h"
#include "../../../lib/logging/VisualLogger.h"
#include "Actions/QuestAction.h"
#include "../pforeach.h"
namespace NKAI
{
ObjectGraphCalculator::ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
:ai(ai), target(target), syncLock()
{
}
void ObjectGraphCalculator::setGraphObjects()
{
for(auto obj : ai->memory->visitableObjs)
{
if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
{
addObjectActor(obj);
}
}
for(auto town : ai->cb->getTownsInfo())
{
addObjectActor(town);
}
}
void ObjectGraphCalculator::calculateConnections()
{
updatePaths();
std::vector<AIPath> pathCache;
foreach_tile_pos(ai->cb.get(), [this, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
{
calculateConnections(pos, pathCache);
});
removeExtraConnections();
}
float ObjectGraphCalculator::getNeighborConnectionsCost(const int3 & pos, std::vector<AIPath> & pathCache)
{
float neighborCost = std::numeric_limits<float>::max();
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Checking junction %s", pos.toString());
}
foreach_neighbour(
ai->cb.get(),
pos,
[this, &neighborCost, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
ai->pathfinder->calculatePathInfo(pathCache, neighbor);
auto costTotal = this->getConnectionsCost(pathCache);
if(costTotal.connectionsCount > 2 && costTotal.avg < neighborCost)
{
neighborCost = costTotal.avg;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Better node found at %s", neighbor.toString());
}
}
});
return neighborCost;
}
void ObjectGraphCalculator::addMinimalDistanceJunctions()
{
tbb::concurrent_unordered_set<int3, std::hash<int3>> junctions;
pforeachTilePaths(ai->cb->getMapSize(), ai, [this, &junctions](const int3 & pos, std::vector<AIPath> & paths)
{
if(target->hasNodeAt(pos))
return;
if(ai->cb->getGuardingCreaturePosition(pos).valid())
return;
ConnectionCostInfo currentCost = getConnectionsCost(paths);
if(currentCost.connectionsCount <= 2)
return;
float neighborCost = getNeighborConnectionsCost(pos, paths);
if(currentCost.avg < neighborCost)
{
junctions.insert(pos);
}
});
for(auto pos : junctions)
{
addJunctionActor(pos);
}
}
void ObjectGraphCalculator::updatePaths()
{
PathfinderSettings ps;
ps.mainTurnDistanceLimit = 5;
ps.scoutTurnDistanceLimit = 1;
ps.allowBypassObjects = false;
ai->pathfinder->updatePaths(actors, ps);
}
void ObjectGraphCalculator::calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache)
{
if(target->hasNodeAt(pos))
{
foreach_neighbour(
ai->cb.get(),
pos,
[this, &pos, &pathCache](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
if(target->hasNodeAt(neighbor))
{
ai->pathfinder->calculatePathInfo(pathCache, neighbor);
for(auto & path : pathCache)
{
if(pos == path.targetHero->visitablePos())
{
target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger());
}
}
}
});
auto obj = ai->cb->getTopObj(pos);
if((obj && obj->ID == Obj::BOAT) || target->isVirtualBoat(pos))
{
ai->pathfinder->calculatePathInfo(pathCache, pos);
for(AIPath & path : pathCache)
{
auto from = path.targetHero->visitablePos();
auto fromObj = actorObjectMap[path.targetHero];
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos, path.targetHero, true);
auto updated = target->tryAddConnection(
from,
pos,
path.movementCost(),
danger);
if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
{
logAi->trace(
"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
fromObj ? fromObj->getObjectName() : "J", from.toString(),
"Boat", pos.toString(),
pos.toString(),
path.movementCost());
}
}
}
return;
}
auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
ai->pathfinder->calculatePathInfo(pathCache, pos);
for(AIPath & path1 : pathCache)
{
for(AIPath & path2 : pathCache)
{
if(path1.targetHero == path2.targetHero)
continue;
auto pos1 = path1.targetHero->visitablePos();
auto pos2 = path2.targetHero->visitablePos();
if(guardPos.valid() && guardPos != pos1 && guardPos != pos2)
continue;
auto obj1 = actorObjectMap[path1.targetHero];
auto obj2 = actorObjectMap[path2.targetHero];
auto tile1 = cb->getTile(pos1);
auto tile2 = cb->getTile(pos2);
if(tile2->isWater() && !tile1->isWater())
{
if(!cb->getTile(pos)->isWater())
continue;
auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
if(!startingObjIsBoat)
continue;
}
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
auto updated = target->tryAddConnection(
pos1,
pos2,
path1.movementCost() + path2.movementCost(),
danger);
if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
{
logAi->trace(
"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
obj1 ? obj1->getObjectName() : "J", pos1.toString(),
obj2 ? obj2->getObjectName() : "J", pos2.toString(),
pos.toString(),
path1.movementCost() + path2.movementCost());
}
}
}
}
bool ObjectGraphCalculator::isExtraConnection(float direct, float side1, float side2) const
{
float sideRatio = (side1 + side2) / direct;
return sideRatio < 1.25f && direct > side1 && direct > side2;
}
void ObjectGraphCalculator::removeExtraConnections()
{
std::vector<std::pair<int3, int3>> connectionsToRemove;
for(auto & actor : temporaryActorHeroes)
{
auto pos = actor->visitablePos();
auto & currentNode = target->getNode(pos);
target->iterateConnections(pos, [this, &pos, &connectionsToRemove, &currentNode](int3 n1, ObjectLink o1)
{
target->iterateConnections(n1, [&pos, &o1, &currentNode, &connectionsToRemove, this](int3 n2, ObjectLink o2)
{
auto direct = currentNode.connections.find(n2);
if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost))
{
connectionsToRemove.push_back({pos, n2});
}
});
});
}
vstd::removeDuplicates(connectionsToRemove);
for(auto & c : connectionsToRemove)
{
target->removeConnection(c.first, c.second);
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString());
}
}
}
void ObjectGraphCalculator::addObjectActor(const CGObjectInstance * obj)
{
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(obj->cb)).get();
CRandomGenerator rng;
auto visitablePos = obj->visitablePos();
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
if(cb->getTile(visitablePos)->isWater())
{
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = obj;
actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::BOAT ? HeroRole::MAIN : HeroRole::SCOUT;
target->addObject(obj);
auto shipyard = dynamic_cast<const IShipyard *>(obj);
if(shipyard && shipyard->bestLocation().valid())
{
int3 virtualBoat = shipyard->bestLocation();
addJunctionActor(virtualBoat, true);
target->addVirtualBoat(virtualBoat, obj);
}
}
void ObjectGraphCalculator::addJunctionActor(const int3 & visitablePos, bool isVirtualBoat)
{
std::lock_guard<std::mutex> lock(syncLock);
auto internalCb = temporaryActorHeroes.front()->cb;
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
CRandomGenerator rng;
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
{
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = nullptr;
actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
target->registerJunction(visitablePos);
}
ConnectionCostInfo ObjectGraphCalculator::getConnectionsCost(std::vector<AIPath> & paths) const
{
std::map<int3, float> costs;
for(auto & path : paths)
{
auto fromPos = path.targetHero->visitablePos();
auto cost = costs.find(fromPos);
if(cost == costs.end())
{
costs.emplace(fromPos, path.movementCost());
}
else
{
if(path.movementCost() < cost->second)
{
costs[fromPos] = path.movementCost();
}
}
}
ConnectionCostInfo result;
for(auto & cost : costs)
{
result.totalCost += cost.second;
result.connectionsCount++;
}
if(result.connectionsCount)
{
result.avg = result.totalCost / result.connectionsCount;
}
return result;
}
}

View File

@ -0,0 +1,56 @@
/*
* ObjectGraphCalculator.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 "ObjectGraph.h"
#include "../AIUtility.h"
namespace NKAI
{
struct ConnectionCostInfo
{
float totalCost = 0;
float avg = 0;
int connectionsCount = 0;
};
class ObjectGraphCalculator
{
private:
ObjectGraph * target;
const Nullkiller * ai;
std::mutex syncLock;
std::map<const CGHeroInstance *, HeroRole> actors;
std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
std::vector<std::unique_ptr<CGBoat>> temporaryBoats;
std::vector<std::unique_ptr<CGHeroInstance>> temporaryActorHeroes;
public:
ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai);
void setGraphObjects();
void calculateConnections();
float getNeighborConnectionsCost(const int3 & pos, std::vector<AIPath> & pathCache);
void addMinimalDistanceJunctions();
private:
void updatePaths();
void calculateConnections(const int3 & pos, std::vector<AIPath> & pathCache);
bool isExtraConnection(float direct, float side1, float side2) const;
void removeExtraConnections();
void addObjectActor(const CGObjectInstance * obj);
void addJunctionActor(const int3 & visitablePos, bool isVirtualBoat = false);
ConnectionCostInfo getConnectionsCost(std::vector<AIPath> & paths) const;
};
}

View File

@ -18,10 +18,11 @@ class VCAI;
namespace Goals
{
template<typename T> class DLL_EXPORT CGoal : public AbstractGoal
template<typename T>
class DLL_EXPORT CGoal : public AbstractGoal
{
public:
CGoal<T>(EGoals goal = INVALID) : AbstractGoal(goal)
CGoal(EGoals goal = INVALID) : AbstractGoal(goal)
{
priority = 0;
isElementar = false;
@ -56,7 +57,7 @@ namespace Goals
return f->evaluate(static_cast<T &>(*this)); //casting enforces template instantiation
}
CGoal<T> * clone() const override
CGoal * clone() const override
{
return new T(static_cast<T const &>(*this)); //casting enforces template instantiation
}

View File

@ -1,5 +1,6 @@
#!/bin/sh
sudo apt remove needrestart
sudo apt-get update
# Dependencies

View File

@ -1,5 +1,6 @@
#!/bin/sh
sudo apt remove needrestart
sudo apt-get update
# Dependencies

View File

@ -1,3 +1,96 @@
# 1.5.1 -> 1.5.2
### Stability
* Fixed crash on closing game while combat or map animations are playing
* Fixed crash on closing game while network thread is waiting for dialog to be closed
* Fixed random crash on starting random map with 'random' number of players
* Fixed crash caused by thread races on loading map list
* Failure to read data from network connection will show up as 'disconnection' and not as a crash
* Fixed a possible crash when replaying a manually played battle with the 'unlimited battle replay' option set
* Fixed crash when loading save made on a 64-bit system or connecting to multiplayer game with a 64-bit host on a 32-bit system (and vice versa)
* Fixed crash when ending a battle in a draw when a hero has the Necromancy skill
* Fixed crash when having SPELL_LIKE_ATTACK bonus with invalid spell ID
* Fixed transfer of non-first artefacts in backpack if hero does not transfer as well
* Game will now abort loading if a corrupt mod is detected instead of crashing without explanation later
### Multiplayer
* Contact between allied players will no longer break simturns
* Having hero in range of object owned by another player will now be registered as contact
* Multiplayer saves are now visible when starting a single player game
* Added chat command '!vote' to initiate a vote to change the duration of simultaneous turns or to change turn timers
* Added chat command '!help' to list all available chat commands
* All multiplayer chat commands now use a leading exclamation mark
### Campaigns
* If the hero attacks an enemy player and is defeated, he will be correctly registered as defeated by the defending player.
* Allow standard victory condition on 'To kill a hero' campaign mission in line with H3
* Fixes Adrienne starting without Inferno spell in campaign
### Interface
* For artefacts that are part of a combined artefact, the game will now show which component of that artefact your hero has.
* Fixed broken in 1.5.1 shortcut for artifact sets saving
* Fixed full screen toggle (F4) not applying changes immediately
* Retaliation preview now accounts for creatures that don't receive retaliations (Sprites, Archdevils, etc)
* Fixed not visible retaliation preview if damage estimation string is longer than battle log line due to long creature name
* Game will now select last save on loading screen
* High Scores screen and Campaign Epilogue screen are now displayed with background on resolutions higher than 800x600
* Fixed non-functioning shortcut 'P' to access Puzzle Map from adventure map
* Added keyboard shortcuts to markets and altars. 'Space' to confirm deal and 'M' to trade maximum possible amount
* Pressing 'Escape' in main menu will now trigger 'Back' and 'Quit' buttons
* Added keyboard shortcuts to hero exchange window:
* * 'F10' will now swap armies
* * 'F11' will now swap artifacts. Additionally, 'Ctrl+F11' will swap equipped artifacts, and 'Shift+F11' will swap backpacks
* * Added unassigned shortcuts to move armies or artifacts to left or right side
* Added keyboard shortcuts to access buildings from town interface:
* * 'F' will now open Fort window
* * 'B' will now open Town Hall window
* * 'G' will now open Mage Guild window
* * 'M' will now open Marketplace
* * 'R' will now open recruitment interface
* * 'T' will now open Tavern window
* * 'G' will now open Thieves Guild
* * 'E' will now open hero exchange screen, if both heroes are present in town
* * 'H' will now open hero screen. Additionally, 'Shift+H' will open garrisoned hero screen, and 'Ctrl+H' will open visiting hero screen
* * 'Space' will now swap visiting and garrisoned heroes
* Added keyboard shortcuts to switch between tabs in Scenario Selection window:
* * 'E' will open Extra Options tab
* * 'T' will open Turn Options tab
* * 'I' will open Invite Players window (only for lobby games)
* * 'R' will now replay video in campaigns
* Added keyboard shortcuts to Adventure map:
* * 'Ctrl+L' will now prompt to open Load Game screen
* * 'Ctrl+M' will now prompt to go to main menu
* * 'Ctrl+N' will now prompt to go to New Game screen
* * 'Ctrl+Q' will now prompt to quit game
* * Page Up, Page Down, Home and End keys will now move hero on adventure map similar to numpad equivalents
* * Fixed non-functioning shortcuts '+' and '-' on numpad to zoom adventure map
* Added keyboard shortcuts to Battle interface:
* * 'V' now allows to view information of hovered unit
* * 'I' now allows to view information of active unit
### Mechanics
* Game will no longer pick creatures exclusive to AB campaigns for random creatures or for Refugee Camp, in line with H3
* If original movement rules are on, it is not possible to attack guards from visitable object directly, only from free tile
* Fixed bug leading that allowed picking up objects while flying on top of water
* Hero can now land when flying from guarded tile to accessible guarded tile irregardless of original movement rules switch
* Interface will now use same arrow for U-turns in path as H3
### AI
* Nullkiller AI can now explore the map
* Nullkiller AI will no longer use the map reveal cheat when allied with a human or when playing on low difficulty
* Nullkiller AI is now used by default for allied players
### Launcher
* When extracting data from gog.com offline installer game will extract files directly into used data directory instead of temporary directory
### Map Editor
* Fixed victory / loss conditions widget initialization
### Modding
* Hero specialties with multiple bonuses that have TIMES_HERO_LEVEL updater now work as expected
* Spells that apply multiple bonuses with same type and subtype but different value type now work as expected
* Added option to toggle layout of guards in creature banks
# 1.5.0 -> 1.5.1
### Stability

View File

@ -242,16 +242,23 @@
"vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。",
"vcmi.battleWindow.pressKeyToSkipIntro" : "按下任意键立即开始战斗",
"vcmi.battleWindow.damageEstimation.melee" : "近战攻击 %CREATURE (%DAMAGE).",
"vcmi.battleWindow.damageEstimation.meleeKills" : "近战攻击 %CREATURE (%DAMAGE, %KILLS).",
"vcmi.battleWindow.damageEstimation.ranged" : "射击 %CREATURE (%SHOTS, %DAMAGE).",
"vcmi.battleWindow.damageEstimation.rangedKills" : "射击 %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
"vcmi.battleWindow.damageEstimation.melee" : "近战攻击 %CREATURE (%DAMAGE)",
"vcmi.battleWindow.damageEstimation.meleeKills" : "近战攻击 %CREATURE (%DAMAGE, %KILLS)",
"vcmi.battleWindow.damageEstimation.ranged" : "射击 %CREATURE (%SHOTS, %DAMAGE)",
"vcmi.battleWindow.damageEstimation.rangedKills" : "射击 %CREATURE (%SHOTS, %DAMAGE, %KILLS)",
"vcmi.battleWindow.damageEstimation.shots" : "%d 弹药剩余",
"vcmi.battleWindow.damageEstimation.shots.1" : "%d 弹药剩余",
"vcmi.battleWindow.damageEstimation.damage" : "%d 伤害",
"vcmi.battleWindow.damageEstimation.damage.1" : "%d 伤害",
"vcmi.battleWindow.damageEstimation.kills" : "%d 将被消灭",
"vcmi.battleWindow.damageEstimation.kills.1" : "%d 将被消灭",
"vcmi.battleWindow.damageRetaliation.will" : "将会反击",
"vcmi.battleWindow.damageRetaliation.may" : "可能反击",
"vcmi.battleWindow.damageRetaliation.never" : "不会反击。",
"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE)。",
"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS)。",
"vcmi.battleWindow.killed" : "已消灭",
"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s 死于精准射击",
"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s 死于精准射击",

View File

@ -252,6 +252,13 @@
"vcmi.battleWindow.damageEstimation.damage.1" : "obrażenia: %d",
"vcmi.battleWindow.damageEstimation.kills" : "zginie: %d",
"vcmi.battleWindow.damageEstimation.kills.1" : "zginie: %d",
"vcmi.battleWindow.damageRetaliation.will" : "Nastąpi odwet ",
"vcmi.battleWindow.damageRetaliation.may" : "Możliwy odwet ",
"vcmi.battleWindow.damageRetaliation.never" : "Nie będzie odwetu.",
"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
"vcmi.battleWindow.killed" : "Zabici",
"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s zostało zabitych poprzez celne strzały!",
"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s został zabity poprzez celny strzał!",

View File

@ -10,8 +10,8 @@ android {
applicationId "is.xyz.vcmi"
minSdk 19
targetSdk 33
versionCode 1515
versionName "1.5.1"
versionCode 1520
versionName "1.5.2"
setProperty("archivesBaseName", "vcmi")
}

View File

@ -138,6 +138,7 @@ set(client_SRCS
windows/CCastleInterface.cpp
windows/CCreatureWindow.cpp
windows/CExchangeWindow.cpp
windows/CHeroOverview.cpp
windows/CHeroWindow.cpp
windows/CKingdomInterface.cpp
@ -332,6 +333,7 @@ set(client_HEADERS
windows/CCastleInterface.h
windows/CCreatureWindow.h
windows/CExchangeWindow.h
windows/CHeroOverview.h
windows/CHeroWindow.h
windows/CKingdomInterface.h

View File

@ -51,6 +51,7 @@
#include "windows/CCastleInterface.h"
#include "windows/CCreatureWindow.h"
#include "windows/CExchangeWindow.h"
#include "windows/CHeroWindow.h"
#include "windows/CKingdomInterface.h"
#include "windows/CMarketWindow.h"
@ -136,7 +137,7 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player):
LOCPLINT = this;
playerID=Player;
human=true;
battleInt = nullptr;
battleInt.reset();
castleInt = nullptr;
makingTurn = false;
showingDialog = new ConditionalWait();

View File

@ -43,6 +43,7 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon
// Check if it's LobbyClientConnected for our client
if(pack.uuid == handler.logicConnection->uuid)
{
handler.logicConnection->setSerializationVersion(pack.version);
handler.logicConnection->connectionID = pack.clientId;
if(handler.mapToStart)
{

View File

@ -886,9 +886,9 @@ void AdventureMapInterface::hotkeySwitchMapLevel()
widget->getMapView()->onMapLevelSwitched();
}
void AdventureMapInterface::hotkeyZoom(int delta)
void AdventureMapInterface::hotkeyZoom(int delta, bool useDeadZone)
{
widget->getMapView()->onMapZoomLevelChanged(delta);
widget->getMapView()->onMapZoomLevelChanged(delta, useDeadZone);
}
void AdventureMapInterface::onScreenResize()

View File

@ -120,7 +120,7 @@ public:
void hotkeyEndingTurn();
void hotkeyNextTown();
void hotkeySwitchMapLevel();
void hotkeyZoom(int delta);
void hotkeyZoom(int delta, bool useDeadZone);
/// Called by PlayerInterface when specified player is ready to start his turn
void onHotseatWaitStarted(PlayerColor playerID);

View File

@ -12,6 +12,7 @@
#include "AdventureMapShortcuts.h"
#include "../CGameInfo.h"
#include "../CMT.h"
#include "../CPlayerInterface.h"
#include "../CServerHandler.h"
#include "../PlayerLocalState.h"
@ -77,21 +78,26 @@ std::vector<AdventureMapShortcutState> AdventureMapShortcuts::getShortcuts()
{ EShortcut::ADVENTURE_CAST_SPELL, optionHeroSelected(), [this]() { this->showSpellbook(); } },
{ EShortcut::ADVENTURE_GAME_OPTIONS, optionInMapView(), [this]() { this->adventureOptions(); } },
{ EShortcut::GLOBAL_OPTIONS, optionInMapView(), [this]() { this->systemOptions(); } },
{ EShortcut::ADVENTURE_FIRST_HERO, optionInMapView(), [this]() { this->firstHero(); } },
{ EShortcut::ADVENTURE_NEXT_HERO, optionHasNextHero(), [this]() { this->nextHero(); } },
{ EShortcut::GAME_END_TURN, optionCanEndTurn(), [this]() { this->endTurn(); } },
{ EShortcut::ADVENTURE_END_TURN, optionCanEndTurn(), [this]() { this->endTurn(); } },
{ EShortcut::ADVENTURE_THIEVES_GUILD, optionInMapView(), [this]() { this->showThievesGuild(); } },
{ EShortcut::ADVENTURE_VIEW_SCENARIO, optionInMapView(), [this]() { this->showScenarioInfo(); } },
{ EShortcut::GAME_SAVE_GAME, optionInMapView(), [this]() { this->saveGame(); } },
{ EShortcut::GAME_LOAD_GAME, optionInMapView(), [this]() { this->loadGame(); } },
{ EShortcut::ADVENTURE_QUIT_GAME, optionInMapView(), [this]() { this->quitGame(); } },
{ EShortcut::ADVENTURE_TO_MAIN_MENU, optionInMapView(), [this]() { this->toMainMenu(); } },
{ EShortcut::ADVENTURE_SAVE_GAME, optionInMapView(), [this]() { this->saveGame(); } },
{ EShortcut::ADVENTURE_NEW_GAME, optionInMapView(), [this]() { this->newGame(); } },
{ EShortcut::ADVENTURE_LOAD_GAME, optionInMapView(), [this]() { this->loadGame(); } },
{ EShortcut::ADVENTURE_RESTART_GAME, optionInMapView(), [this]() { this->restartGame(); } },
{ EShortcut::ADVENTURE_DIG_GRAIL, optionHeroSelected(), [this]() { this->digGrail(); } },
{ EShortcut::ADVENTURE_VIEW_PUZZLE, optionSidePanelActive(),[this]() { this->viewPuzzleMap(); } },
{ EShortcut::GAME_RESTART_GAME, optionInMapView(), [this]() { this->restartGame(); } },
{ EShortcut::ADVENTURE_VISIT_OBJECT, optionCanVisitObject(), [this]() { this->visitObject(); } },
{ EShortcut::ADVENTURE_VIEW_SELECTED, optionInMapView(), [this]() { this->openObject(); } },
{ EShortcut::GAME_OPEN_MARKETPLACE, optionInMapView(), [this]() { this->showMarketplace(); } },
{ EShortcut::ADVENTURE_ZOOM_IN, optionSidePanelActive(),[this]() { this->zoom(+1); } },
{ EShortcut::ADVENTURE_ZOOM_OUT, optionSidePanelActive(),[this]() { this->zoom(-1); } },
{ EShortcut::ADVENTURE_MARKETPLACE, optionInMapView(), [this]() { this->showMarketplace(); } },
{ EShortcut::ADVENTURE_ZOOM_IN, optionSidePanelActive(),[this]() { this->zoom(+10); } },
{ EShortcut::ADVENTURE_ZOOM_OUT, optionSidePanelActive(),[this]() { this->zoom(-10); } },
{ EShortcut::ADVENTURE_ZOOM_RESET, optionSidePanelActive(),[this]() { this->zoom( 0); } },
{ EShortcut::ADVENTURE_FIRST_TOWN, optionInMapView(), [this]() { this->firstTown(); } },
{ EShortcut::ADVENTURE_NEXT_TOWN, optionInMapView(), [this]() { this->nextTown(); } },
{ EShortcut::ADVENTURE_NEXT_OBJECT, optionInMapView(), [this]() { this->nextObject(); } },
{ EShortcut::ADVENTURE_MOVE_HERO_SW, optionHeroSelected(), [this]() { this->moveHeroDirectional({-1, +1}); } },
@ -223,6 +229,16 @@ void AdventureMapShortcuts::systemOptions()
GH.windows().createAndPushWindow<SettingsMainWindow>();
}
void AdventureMapShortcuts::firstHero()
{
if (!LOCPLINT->localState->getWanderingHeroes().empty())
{
const auto * hero = LOCPLINT->localState->getWanderingHero(0);
LOCPLINT->localState->setSelection(hero);
owner.centerOnObject(hero);
}
}
void AdventureMapShortcuts::nextHero()
{
const auto * currHero = LOCPLINT->localState->getCurrentHero();
@ -288,6 +304,49 @@ void AdventureMapShortcuts::showScenarioInfo()
AdventureOptions::showScenarioInfo();
}
void AdventureMapShortcuts::toMainMenu()
{
LOCPLINT->showYesNoDialog(
CGI->generaltexth->allTexts[578],
[]()
{
CSH->endGameplay();
GH.defActionsDef = 63;
CMM->menu->switchToTab("main");
},
0
);
}
void AdventureMapShortcuts::newGame()
{
LOCPLINT->showYesNoDialog(
CGI->generaltexth->allTexts[578],
[]()
{
CSH->endGameplay();
GH.defActionsDef = 63;
CMM->menu->switchToTab("new");
},
nullptr
);
}
void AdventureMapShortcuts::quitGame()
{
LOCPLINT->showYesNoDialog(
CGI->generaltexth->allTexts[578],
[]()
{
GH.dispatchMainThread( []()
{
handleQuit(false);
});
},
0
);
}
void AdventureMapShortcuts::saveGame()
{
GH.windows().createAndPushWindow<CSavingScreen>();
@ -366,6 +425,16 @@ void AdventureMapShortcuts::showMarketplace()
LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
}
void AdventureMapShortcuts::firstTown()
{
if (!LOCPLINT->localState->getOwnedTowns().empty())
{
const auto * town = LOCPLINT->localState->getOwnedTown(0);
LOCPLINT->localState->setSelection(town);
owner.centerOnObject(town);
}
}
void AdventureMapShortcuts::nextTown()
{
owner.hotkeyNextTown();
@ -373,7 +442,7 @@ void AdventureMapShortcuts::nextTown()
void AdventureMapShortcuts::zoom( int distance)
{
owner.hotkeyZoom(distance);
owner.hotkeyZoom(distance, false);
}
void AdventureMapShortcuts::nextObject()
@ -494,7 +563,7 @@ bool AdventureMapShortcuts::optionInWorldView()
bool AdventureMapShortcuts::optionSidePanelActive()
{
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
}
bool AdventureMapShortcuts::optionMapScrollingActive()

View File

@ -49,10 +49,14 @@ class AdventureMapShortcuts
void showSpellbook();
void adventureOptions();
void systemOptions();
void firstHero();
void nextHero();
void endTurn();
void showThievesGuild();
void showScenarioInfo();
void toMainMenu();
void newGame();
void quitGame();
void saveGame();
void loadGame();
void digGrail();
@ -61,6 +65,7 @@ class AdventureMapShortcuts
void visitObject();
void openObject();
void showMarketplace();
void firstTown();
void nextTown();
void nextObject();
void zoom( int distance);

View File

@ -45,7 +45,7 @@ AdventureOptions::AdventureOptions()
scenInfo = std::make_shared<CButton>(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
scenInfo->addCallback(AdventureOptions::showScenarioInfo);
replay = std::make_shared<CButton>(Point(24, 257), AnimationPath::builtin("ADVTURN.DEF"), CButton::tooltip(), [&](){ close(); });
replay = std::make_shared<CButton>(Point(24, 257), AnimationPath::builtin("ADVTURN.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_REPLAY_TURN);
replay->addCallback([]{ LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.replayOpponentTurnNotImplemented")); });
exit = std::make_shared<CButton>(Point(203, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);

View File

@ -158,9 +158,6 @@ void CInGameConsole::keyPressed (EShortcut key)
break;
case EShortcut::GAME_ACTIVATE_CONSOLE:
if(GH.isKeyboardAltDown())
return; //QoL for alt-tab operating system shortcut
if(!enteredText.empty())
endEnteringText(false);
else

View File

@ -332,7 +332,7 @@ void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID)
GH.windows().pushWindow(wnd);
curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897
CPlayerInterface::battleInt = nullptr;
CPlayerInterface::battleInt.reset();
}
void BattleInterface::spellCast(const BattleSpellCast * sc)

View File

@ -82,6 +82,13 @@ std::vector<std::string> BattleConsole::getVisibleText()
auto result = CMessage::breakText(text, pos.w, FONT_SMALL);
if(result.size() > 2 && text.find('\n') != std::string::npos)
{
// Text has too many lines to fit into console, but has line breaks. Try ignore them and fit text that way
std::string cleanText = boost::algorithm::replace_all_copy(text, "\n", " ");
result = CMessage::breakText(cleanText, pos.w, FONT_SMALL);
}
if(result.size() > 2)
result.resize(2);
return result;

View File

@ -27,6 +27,7 @@
#include "../widgets/Buttons.h"
#include "../widgets/Images.h"
#include "../windows/CMessage.h"
#include "../windows/CCreatureWindow.h"
#include "../render/CAnimation.h"
#include "../render/Canvas.h"
#include "../render/IRenderHandler.h"
@ -77,6 +78,8 @@ BattleWindow::BattleWindow(BattleInterface & owner):
addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this));
addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this));
addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this));
addShortcut(EShortcut::BATTLE_OPEN_ACTIVE_UNIT, std::bind(&BattleWindow::bOpenActiveUnit, this));
addShortcut(EShortcut::BATTLE_OPEN_HOVERED_UNIT, std::bind(&BattleWindow::bOpenHoveredUnit, this));
addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();});
addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();});
@ -189,11 +192,6 @@ void BattleWindow::createTimerInfoWindows()
}
}
BattleWindow::~BattleWindow()
{
CPlayerInterface::battleInt = nullptr;
}
std::shared_ptr<BattleConsole> BattleWindow::buildBattleConsole(const JsonNode & config) const
{
auto rect = readRect(config["rect"]);
@ -755,6 +753,8 @@ void BattleWindow::blockUI(bool on)
bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false;
setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on);
setShortcutBlocked(EShortcut::BATTLE_OPEN_ACTIVE_UNIT, on);
setShortcutBlocked(EShortcut::BATTLE_OPEN_HOVERED_UNIT, on);
setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee());
setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0);
setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
@ -769,6 +769,26 @@ void BattleWindow::blockUI(bool on)
setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
}
void BattleWindow::bOpenActiveUnit()
{
const auto * unit = owner.stacksController->getActiveStack();
if (unit)
GH.windows().createAndPushWindow<CStackWindow>(unit, false);;
}
void BattleWindow::bOpenHoveredUnit()
{
const auto units = owner.stacksController->getHoveredStacksUnitIds();
if (!units.empty())
{
const auto * unit = owner.getBattle()->battleGetStackByID(units[0]);
if (unit)
GH.windows().createAndPushWindow<CStackWindow>(unit, false);
}
}
std::optional<uint32_t> BattleWindow::getQueueHoveredUnitId()
{
return queue->getHoveredUnitIdIfAny();

View File

@ -56,6 +56,8 @@ class BattleWindow : public InterfaceObjectConfigurable
void bConsoleDownf();
void bTacticNextStack();
void bTacticPhaseEnd();
void bOpenActiveUnit();
void bOpenHoveredUnit();
/// functions for handling actions after they were confirmed by popup window
void reallyFlee();
@ -80,7 +82,6 @@ class BattleWindow : public InterfaceObjectConfigurable
public:
BattleWindow(BattleInterface & owner );
~BattleWindow();
/// Closes window once battle finished
void close();

View File

@ -164,6 +164,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
{
if(ev.key.keysym.sym == SDLK_F4 && (ev.key.keysym.mod & KMOD_ALT))
{
// FIXME: dead code? Looks like intercepted by OS/SDL and delivered as SDL_Quit instead?
boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
handleQuit(true);
return;
@ -175,16 +176,6 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
handleQuit(true);
return;
}
if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F4)
{
boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
Settings full = settings.write["video"]["fullscreen"];
full->Bool() = !full->Bool();
GH.onScreenResize(false);
return;
}
}
else if(ev.type == SDL_USEREVENT)
{

View File

@ -75,9 +75,16 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
auto shortcutsVector = GH.shortcuts().translateKeycode(keyName);
if (vstd::contains(shortcutsVector, EShortcut::LOBBY_ACTIVATE_INTERFACE))
if (vstd::contains(shortcutsVector, EShortcut::MAIN_MENU_LOBBY))
CSH->getGlobalLobby().activateInterface();
if (vstd::contains(shortcutsVector, EShortcut::GLOBAL_FULLSCREEN))
{
Settings full = settings.write["video"]["fullscreen"];
full->Bool() = !full->Bool();
GH.onScreenResize(true);
}
if (vstd::contains(shortcutsVector, EShortcut::SPECTATE_TRACK_HERO))
{
Settings s = settings.write["session"];

View File

@ -15,6 +15,7 @@
#include "../CServerHandler.h"
#include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h"
#include "../widgets/Buttons.h"
#include "../widgets/GraphicalPrimitiveCanvas.h"
#include "../widgets/Images.h"
@ -95,7 +96,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 324), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, 0, 0, 1 | 4, Rect(200, 0, 320, 320));
buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); } );
buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); }, EShortcut::GLOBAL_RETURN );
center();
}

View File

@ -12,12 +12,11 @@
#include "GlobalLobbyLoginWindow.h"
#include "GlobalLobbyClient.h"
#include "GlobalLobbyWindow.h"
#include "../CGameInfo.h"
#include "../CServerHandler.h"
#include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h"
#include "../gui/Shortcut.h"
#include "../widgets/Buttons.h"
#include "../widgets/CTextInput.h"
#include "../widgets/Images.h"
@ -47,8 +46,8 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString());
backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, ETextAlignment::CENTERLEFT, true);
buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); });
buttonClose = std::make_shared<CButton>(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); }, EShortcut::GLOBAL_ACCEPT);
buttonClose = std::make_shared<CButton>(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL);
labelStatus = std::make_shared<CTextBox>( "", Rect(15, 115, 255, 60), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
auto buttonRegister = std::make_shared<CToggleButton>(Point(10, 40), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);

View File

@ -18,6 +18,7 @@
#include "../CGameInfo.h"
#include "../CServerHandler.h"
#include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h"
#include "../mainmenu/CMainMenu.h"
#include "../widgets/Buttons.h"
#include "../widgets/Images.h"
@ -25,10 +26,8 @@
#include "../widgets/GraphicalPrimitiveCanvas.h"
#include "../widgets/ObjectLists.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CGeneralTextHandler.h"
#include "../../lib/MetaString.h"
#include "../../lib/VCMI_Lib.h"
#include "../../lib/modding/CModHandler.h"
#include "../../lib/modding/CModInfo.h"
@ -148,8 +147,8 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
labelVersionTitle = std::make_shared<CLabel>( 10, 60, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.version").toString());
labelVersionValue = std::make_shared<CLabel>( 10, 80, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.gameVersion);
buttonJoin = std::make_shared<CButton>(Point(10, 360), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onJoin(); });
buttonClose = std::make_shared<CButton>(Point(100, 360), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
buttonJoin = std::make_shared<CButton>(Point(10, 360), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onJoin(); }, EShortcut::GLOBAL_ACCEPT);
buttonClose = std::make_shared<CButton>(Point(100, 360), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL);
MetaString joinStatusText;
std::string errorMessage = getJoinRoomErrorMessage(roomDescription, modVerificationList);

View File

@ -16,6 +16,7 @@
#include "../CGameInfo.h"
#include "../CServerHandler.h"
#include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h"
#include "../mainmenu/CMainMenu.h"
#include "../widgets/Buttons.h"
#include "../widgets/Images.h"
@ -74,8 +75,8 @@ GlobalLobbyServerSetup::GlobalLobbyServerSetup()
labelDescription = std::make_shared<CTextBox>("", Rect(10, 195, pos.w - 20, 80), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
buttonCreate = std::make_shared<CButton>(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); });
buttonClose = std::make_shared<CButton>(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
buttonCreate = std::make_shared<CButton>(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); }, EShortcut::GLOBAL_ACCEPT);
buttonClose = std::make_shared<CButton>(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL);
filledBackground->playerColored(PlayerColor(1));

View File

@ -492,7 +492,7 @@ void InterfaceObjectConfigurable::loadButtonHotkey(std::shared_ptr<CButton> butt
return;
button->addCallback(target->second.callback);
target->second.assignedToButton = true;
target->second.assignedButtons.push_back(button);
}
std::shared_ptr<CLabelGroup> InterfaceObjectConfigurable::buildLabelGroup(const JsonNode & config) const
@ -821,8 +821,9 @@ void InterfaceObjectConfigurable::keyPressed(EShortcut key)
if (target == shortcuts.end())
return;
if (target->second.assignedToButton)
return; // will be handled by button instance
for (auto const & button :target->second.assignedButtons)
if (button->isActive())
return; // will be handled by button instance
if (target->second.blocked)
return;

View File

@ -118,7 +118,7 @@ private:
struct ShortcutState
{
std::function<void()> callback;
mutable bool assignedToButton = false;
mutable std::vector<std::shared_ptr<CButton>> assignedButtons;
bool blocked = false;
};

View File

@ -66,25 +66,55 @@ enum class EShortcut
MAIN_MENU_CAMPAIGN_AB,
MAIN_MENU_CAMPAIGN_CUSTOM,
MAIN_MENU_HOTSEAT,
MAIN_MENU_LOBBY,
MAIN_MENU_HOST_GAME,
MAIN_MENU_JOIN_GAME,
HIGH_SCORES_CAMPAIGNS,
HIGH_SCORES_SCENARIOS,
HIGH_SCORES_RESET,
// Game lobby / scenario selection
LOBBY_BEGIN_STANDARD_GAME, // b
LOBBY_BEGIN_CAMPAIGN, // Return
LOBBY_LOAD_GAME, // l, Return
LOBBY_SAVE_GAME, // s, Return
LOBBY_RANDOM_MAP, // Open random map tab
LOBBY_HIDE_CHAT,
LOBBY_TOGGLE_CHAT,
LOBBY_ADDITIONAL_OPTIONS, // Open additional options tab
LOBBY_SELECT_SCENARIO, // Open map list tab
LOBBY_REPLAY_VIDEO,
LOBBY_EXTRA_OPTIONS,
LOBBY_TURN_OPTIONS,
LOBBY_INVITE_PLAYERS,
LOBBY_FLIP_COIN,
LOBBY_RANDOM_TOWN,
LOBBY_RANDOM_TOWN_VS,
MAPS_SIZE_S,
MAPS_SIZE_M,
MAPS_SIZE_L,
MAPS_SIZE_XL,
MAPS_SIZE_ALL,
MAPS_SORT_PLAYERS,
MAPS_SORT_SIZE,
MAPS_SORT_FORMAT,
MAPS_SORT_NAME,
MAPS_SORT_VICTORY,
MAPS_SORT_DEFEAT,
MAPS_SORT_MAPS,
MAPS_SORT_CHANGEDATE,
SETTINGS_LOAD_GAME,
SETTINGS_SAVE_GAME,
SETTINGS_NEW_GAME,
SETTINGS_RESTART_GAME,
SETTINGS_TO_MAIN_MENU,
SETTINGS_QUIT_GAME,
// In-game hotkeys, require game state but may be available in windows other than adventure map
GAME_END_TURN,
GAME_LOAD_GAME,
GAME_SAVE_GAME,
GAME_RESTART_GAME,
GAME_TO_MAIN_MENU,
GAME_QUIT_GAME,
GAME_OPEN_MARKETPLACE,
GAME_OPEN_THIEVES_GUILD,
GAME_ACTIVATE_CONSOLE, // Tab, activates in-game console
// Adventure map screen
@ -98,10 +128,11 @@ enum class EShortcut
ADVENTURE_VIEW_SELECTED,// Open window with currently selected hero/town
ADVENTURE_NEXT_TOWN,
ADVENTURE_NEXT_HERO,
ADVENTURE_NEXT_OBJECT, // TODO: context-sensitive next object - select next hero/town, depending on current selection
ADVENTURE_FIRST_TOWN, // TODO: select first available town in the list
ADVENTURE_FIRST_HERO, // TODO: select first available hero in the list
ADVENTURE_NEXT_OBJECT, // context-sensitive next object - select next hero/town, depending on current selection
ADVENTURE_FIRST_TOWN, // select first available town in the list
ADVENTURE_FIRST_HERO, // select first available hero in the list
ADVENTURE_VIEW_SCENARIO,// View Scenario Information window
ADVENTURE_REPLAY_TURN,
ADVENTURE_DIG_GRAIL,
ADVENTURE_VIEW_PUZZLE,
ADVENTURE_VIEW_WORLD,
@ -113,11 +144,19 @@ enum class EShortcut
ADVENTURE_KINGDOM_OVERVIEW,
ADVENTURE_QUEST_LOG,
ADVENTURE_CAST_SPELL,
ADVENTURE_MARKETPLACE,
ADVENTURE_THIEVES_GUILD,
ADVENTURE_EXIT_WORLD_VIEW,
ADVENTURE_ZOOM_IN,
ADVENTURE_ZOOM_OUT,
ADVENTURE_ZOOM_RESET,
ADVENTURE_END_TURN,
ADVENTURE_LOAD_GAME,
ADVENTURE_SAVE_GAME,
ADVENTURE_NEW_GAME,
ADVENTURE_RESTART_GAME,
ADVENTURE_TO_MAIN_MENU,
ADVENTURE_QUIT_GAME,
// Move hero one tile in specified direction. Bound to cursors & numpad buttons
ADVENTURE_MOVE_HERO_SW,
@ -145,8 +184,20 @@ enum class EShortcut
BATTLE_TACTICS_END,
BATTLE_SELECT_ACTION, // Alternative actions toggle
BATTLE_TOGGLE_HEROES_STATS,
BATTLE_OPEN_ACTIVE_UNIT,
BATTLE_OPEN_HOVERED_UNIT,
LOBBY_ACTIVATE_INTERFACE,
MARKET_DEAL,
MARKET_MAX_AMOUNT,
MARKET_SACRIFICE_ALL,
MARKET_SACRIFICE_BACKPACK,
MARKET_RESOURCE_PLAYER,
MARKET_ARTIFACT_RESOURCE,
MARKET_RESOURCE_ARTIFACT,
MARKET_CREATURE_RESOURCE,
MARKET_RESOURCE_RESOURCE,
MARKET_CREATURE_EXPERIENCE,
MARKET_ARTIFACT_EXPERIENCE,
SPECTATE_TRACK_HERO,
SPECTATE_SKIP_BATTLE,
@ -154,11 +205,22 @@ enum class EShortcut
// Town screen
TOWN_OPEN_TAVERN,
TOWN_OPEN_HALL,
TOWN_OPEN_FORT,
TOWN_OPEN_MARKET,
TOWN_OPEN_MAGE_GUILD,
TOWN_OPEN_THIEVES_GUILD,
TOWN_OPEN_RECRUITMENT,
TOWN_OPEN_HERO_EXCHANGE,
TOWN_OPEN_HERO,
TOWN_OPEN_VISITING_HERO,
TOWN_OPEN_GARRISONED_HERO,
TOWN_SWAP_ARMIES, // Swap garrisoned and visiting armies
// Creature & creature recruitment screen
RECRUITMENT_MAX, // Set number of creatures to recruit to max
RECRUITMENT_MIN, // Set number of creatures to recruit to min (1)
RECRUITMENT_SWITCH_LEVEL,
RECRUITMENT_UPGRADE, // Upgrade current creature
RECRUITMENT_UPGRADE_ALL, // Upgrade all creatures (Hill Fort / Skeleton Transformer)
@ -172,17 +234,44 @@ enum class EShortcut
HERO_LOOSE_FORMATION,
HERO_TIGHT_FORMATION,
HERO_TOGGLE_TACTICS, // b
HERO_ARMY_SPLIT,
HERO_BACKPACK,
HERO_COSTUME_0,
HERO_COSTUME_1,
HERO_COSTUME_2,
HERO_COSTUME_3,
HERO_COSTUME_4,
HERO_COSTUME_5,
HERO_COSTUME_6,
HERO_COSTUME_7,
HERO_COSTUME_8,
HERO_COSTUME_9,
HERO_COSTUME_SAVE_0,
HERO_COSTUME_SAVE_1,
HERO_COSTUME_SAVE_2,
HERO_COSTUME_SAVE_3,
HERO_COSTUME_SAVE_4,
HERO_COSTUME_SAVE_5,
HERO_COSTUME_SAVE_6,
HERO_COSTUME_SAVE_7,
HERO_COSTUME_SAVE_8,
HERO_COSTUME_SAVE_9,
HERO_COSTUME_LOAD_0,
HERO_COSTUME_LOAD_1,
HERO_COSTUME_LOAD_2,
HERO_COSTUME_LOAD_3,
HERO_COSTUME_LOAD_4,
HERO_COSTUME_LOAD_5,
HERO_COSTUME_LOAD_6,
HERO_COSTUME_LOAD_7,
HERO_COSTUME_LOAD_8,
HERO_COSTUME_LOAD_9,
EXCHANGE_ARMY_TO_LEFT,
EXCHANGE_ARMY_TO_RIGHT,
EXCHANGE_ARMY_SWAP,
EXCHANGE_ARTIFACTS_TO_LEFT,
EXCHANGE_ARTIFACTS_TO_RIGHT,
EXCHANGE_ARTIFACTS_SWAP,
EXCHANGE_EQUIPPED_TO_LEFT,
EXCHANGE_EQUIPPED_TO_RIGHT,
EXCHANGE_EQUIPPED_SWAP,
EXCHANGE_BACKPACK_TO_LEFT,
EXCHANGE_BACKPACK_TO_RIGHT,
EXCHANGE_BACKPACK_SWAP,
EXCHANGE_BACKPACK_LEFT,
EXCHANGE_BACKPACK_RIGHT,
// Spellbook screen
SPELLBOOK_TAB_ADVENTURE,

View File

@ -22,6 +22,25 @@ ShortcutHandler::ShortcutHandler()
mappedKeyboardShortcuts = loadShortcuts(config["keyboard"]);
mappedJoystickShortcuts = loadShortcuts(config["joystickButtons"]);
mappedJoystickAxes = loadShortcuts(config["joystickAxes"]);
#ifndef ENABLE_GOLDMASTER
std::vector<EShortcut> assignedShortcuts;
std::vector<EShortcut> missingShortcuts;
for (auto const & entry : config["keyboard"].Struct())
{
EShortcut shortcutID = findShortcut(entry.first);
assert(!vstd::contains(assignedShortcuts, shortcutID));
assignedShortcuts.push_back(shortcutID);
}
for (EShortcut id = vstd::next(EShortcut::NONE, 1); id < EShortcut::AFTER_LAST; id = vstd::next(id, 1))
if (!vstd::contains(assignedShortcuts, id))
missingShortcuts.push_back(id);
if (!missingShortcuts.empty())
logGlobal->error("Found %d shortcuts without config entry!", missingShortcuts.size());
#endif
}
std::multimap<std::string, EShortcut> ShortcutHandler::loadShortcuts(const JsonNode & data) const
@ -131,22 +150,24 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
{"mainMenuCampaignRoe", EShortcut::MAIN_MENU_CAMPAIGN_ROE },
{"mainMenuCampaignAb", EShortcut::MAIN_MENU_CAMPAIGN_AB },
{"mainMenuCampaignCustom", EShortcut::MAIN_MENU_CAMPAIGN_CUSTOM },
{"mainMenuLobby", EShortcut::MAIN_MENU_LOBBY },
{"lobbyBeginStandardGame", EShortcut::LOBBY_BEGIN_STANDARD_GAME },
{"lobbyBeginCampaign", EShortcut::LOBBY_BEGIN_CAMPAIGN },
{"lobbyLoadGame", EShortcut::LOBBY_LOAD_GAME },
{"lobbySaveGame", EShortcut::LOBBY_SAVE_GAME },
{"lobbyRandomMap", EShortcut::LOBBY_RANDOM_MAP },
{"lobbyHideChat", EShortcut::LOBBY_HIDE_CHAT },
{"lobbyToggleChat", EShortcut::LOBBY_TOGGLE_CHAT },
{"lobbyAdditionalOptions", EShortcut::LOBBY_ADDITIONAL_OPTIONS },
{"lobbySelectScenario", EShortcut::LOBBY_SELECT_SCENARIO },
{"gameEndTurn", EShortcut::GAME_END_TURN },
{"gameLoadGame", EShortcut::GAME_LOAD_GAME },
{"gameSaveGame", EShortcut::GAME_SAVE_GAME },
{"gameRestartGame", EShortcut::GAME_RESTART_GAME },
{"gameMainMenu", EShortcut::GAME_TO_MAIN_MENU },
{"gameQuitGame", EShortcut::GAME_QUIT_GAME },
{"gameOpenMarketplace", EShortcut::GAME_OPEN_MARKETPLACE },
{"gameOpenThievesGuild", EShortcut::GAME_OPEN_THIEVES_GUILD },
{"gameEndTurn", EShortcut::ADVENTURE_END_TURN }, // compatibility ID - extra's use this string
{"adventureEndTurn", EShortcut::ADVENTURE_END_TURN },
{"adventureLoadGame", EShortcut::ADVENTURE_LOAD_GAME },
{"adventureSaveGame", EShortcut::ADVENTURE_SAVE_GAME },
{"adventureRestartGame", EShortcut::ADVENTURE_RESTART_GAME },
{"adventureMainMenu", EShortcut::ADVENTURE_TO_MAIN_MENU },
{"adventureQuitGame", EShortcut::ADVENTURE_QUIT_GAME },
{"adventureMarketplace", EShortcut::ADVENTURE_MARKETPLACE },
{"adventureThievesGuild", EShortcut::ADVENTURE_THIEVES_GUILD },
{"gameActivateConsole", EShortcut::GAME_ACTIVATE_CONSOLE },
{"adventureGameOptions", EShortcut::ADVENTURE_GAME_OPTIONS },
{"adventureToggleGrid", EShortcut::ADVENTURE_TOGGLE_GRID },
@ -201,7 +222,6 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
{"battleTacticsNext", EShortcut::BATTLE_TACTICS_NEXT },
{"battleTacticsEnd", EShortcut::BATTLE_TACTICS_END },
{"battleSelectAction", EShortcut::BATTLE_SELECT_ACTION },
{"lobbyActivateInterface", EShortcut::LOBBY_ACTIVATE_INTERFACE },
{"spectateTrackHero", EShortcut::SPECTATE_TRACK_HERO },
{"spectateSkipBattle", EShortcut::SPECTATE_SKIP_BATTLE },
{"spectateSkipBattleResult", EShortcut::SPECTATE_SKIP_BATTLE_RESULT },
@ -218,20 +238,118 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
{"heroLooseFormation", EShortcut::HERO_LOOSE_FORMATION },
{"heroTightFormation", EShortcut::HERO_TIGHT_FORMATION },
{"heroToggleTactics", EShortcut::HERO_TOGGLE_TACTICS },
{"heroCostume0", EShortcut::HERO_COSTUME_0 },
{"heroCostume1", EShortcut::HERO_COSTUME_1 },
{"heroCostume2", EShortcut::HERO_COSTUME_2 },
{"heroCostume3", EShortcut::HERO_COSTUME_3 },
{"heroCostume4", EShortcut::HERO_COSTUME_4 },
{"heroCostume5", EShortcut::HERO_COSTUME_5 },
{"heroCostume6", EShortcut::HERO_COSTUME_6 },
{"heroCostume7", EShortcut::HERO_COSTUME_7 },
{"heroCostume8", EShortcut::HERO_COSTUME_8 },
{"heroCostume9", EShortcut::HERO_COSTUME_9 },
{"heroCostumeSave0", EShortcut::HERO_COSTUME_SAVE_0 },
{"heroCostumeSave1", EShortcut::HERO_COSTUME_SAVE_1 },
{"heroCostumeSave2", EShortcut::HERO_COSTUME_SAVE_2 },
{"heroCostumeSave3", EShortcut::HERO_COSTUME_SAVE_3 },
{"heroCostumeSave4", EShortcut::HERO_COSTUME_SAVE_4 },
{"heroCostumeSave5", EShortcut::HERO_COSTUME_SAVE_5 },
{"heroCostumeSave6", EShortcut::HERO_COSTUME_SAVE_6 },
{"heroCostumeSave7", EShortcut::HERO_COSTUME_SAVE_7 },
{"heroCostumeSave8", EShortcut::HERO_COSTUME_SAVE_8 },
{"heroCostumeSave9", EShortcut::HERO_COSTUME_SAVE_9 },
{"heroCostumeLoad0", EShortcut::HERO_COSTUME_LOAD_0 },
{"heroCostumeLoad1", EShortcut::HERO_COSTUME_LOAD_1 },
{"heroCostumeLoad2", EShortcut::HERO_COSTUME_LOAD_2 },
{"heroCostumeLoad3", EShortcut::HERO_COSTUME_LOAD_3 },
{"heroCostumeLoad4", EShortcut::HERO_COSTUME_LOAD_4 },
{"heroCostumeLoad5", EShortcut::HERO_COSTUME_LOAD_5 },
{"heroCostumeLoad6", EShortcut::HERO_COSTUME_LOAD_6 },
{"heroCostumeLoad7", EShortcut::HERO_COSTUME_LOAD_7 },
{"heroCostumeLoad8", EShortcut::HERO_COSTUME_LOAD_8 },
{"heroCostumeLoad9", EShortcut::HERO_COSTUME_LOAD_9 },
{"spellbookTabAdventure", EShortcut::SPELLBOOK_TAB_ADVENTURE },
{"spellbookTabCombat", EShortcut::SPELLBOOK_TAB_COMBAT }
{"spellbookTabCombat", EShortcut::SPELLBOOK_TAB_COMBAT },
{"mainMenuHotseat", EShortcut::MAIN_MENU_HOTSEAT },
{"mainMenuHostGame", EShortcut::MAIN_MENU_HOST_GAME },
{"mainMenuJoinGame", EShortcut::MAIN_MENU_JOIN_GAME },
{"highScoresCampaigns", EShortcut::HIGH_SCORES_CAMPAIGNS },
{"highScoresScenarios", EShortcut::HIGH_SCORES_SCENARIOS },
{"highScoresReset", EShortcut::HIGH_SCORES_RESET },
{"lobbyReplayVideo", EShortcut::LOBBY_REPLAY_VIDEO },
{"lobbyExtraOptions", EShortcut::LOBBY_EXTRA_OPTIONS },
{"lobbyTurnOptions", EShortcut::LOBBY_TURN_OPTIONS },
{"lobbyInvitePlayers", EShortcut::LOBBY_INVITE_PLAYERS },
{"lobbyFlipCoin", EShortcut::LOBBY_FLIP_COIN },
{"lobbyRandomTown", EShortcut::LOBBY_RANDOM_TOWN },
{"lobbyRandomTownVs", EShortcut::LOBBY_RANDOM_TOWN_VS },
{"mapsSizeS", EShortcut::MAPS_SIZE_S },
{"mapsSizeM", EShortcut::MAPS_SIZE_M },
{"mapsSizeL", EShortcut::MAPS_SIZE_L },
{"mapsSizeXl", EShortcut::MAPS_SIZE_XL },
{"mapsSizeAll", EShortcut::MAPS_SIZE_ALL },
{"mapsSortPlayers", EShortcut::MAPS_SORT_PLAYERS },
{"mapsSortSize", EShortcut::MAPS_SORT_SIZE },
{"mapsSortFormat", EShortcut::MAPS_SORT_FORMAT },
{"mapsSortName", EShortcut::MAPS_SORT_NAME },
{"mapsSortVictory", EShortcut::MAPS_SORT_VICTORY },
{"mapsSortDefeat", EShortcut::MAPS_SORT_DEFEAT },
{"mapsSortMaps", EShortcut::MAPS_SORT_MAPS },
{"mapsSortChangedate", EShortcut::MAPS_SORT_CHANGEDATE },
{"settingsLoadGame", EShortcut::SETTINGS_LOAD_GAME },
{"settingsSaveGame", EShortcut::SETTINGS_SAVE_GAME },
{"settingsNewGame", EShortcut::SETTINGS_NEW_GAME },
{"settingsRestartGame", EShortcut::SETTINGS_RESTART_GAME },
{"settingsToMainMenu", EShortcut::SETTINGS_TO_MAIN_MENU },
{"settingsQuitGame", EShortcut::SETTINGS_QUIT_GAME },
{"adventureReplayTurn", EShortcut::ADVENTURE_REPLAY_TURN },
{"adventureNewGame", EShortcut::ADVENTURE_NEW_GAME },
{"battleOpenActiveUnit", EShortcut::BATTLE_OPEN_ACTIVE_UNIT },
{"battleOpenHoveredUnit", EShortcut::BATTLE_OPEN_HOVERED_UNIT },
{"marketDeal", EShortcut::MARKET_DEAL },
{"marketMaxAmount", EShortcut::MARKET_MAX_AMOUNT },
{"marketSacrificeAll", EShortcut::MARKET_SACRIFICE_ALL },
{"marketSacrificeBackpack", EShortcut::MARKET_SACRIFICE_BACKPACK },
{"marketResourcePlayer", EShortcut::MARKET_RESOURCE_PLAYER },
{"marketArtifactResource", EShortcut::MARKET_ARTIFACT_RESOURCE },
{"marketResourceArtifact", EShortcut::MARKET_RESOURCE_ARTIFACT },
{"marketCreatureResource", EShortcut::MARKET_CREATURE_RESOURCE },
{"marketResourceResource", EShortcut::MARKET_RESOURCE_RESOURCE },
{"marketCreatureExperience", EShortcut::MARKET_CREATURE_EXPERIENCE },
{"marketArtifactExperience", EShortcut::MARKET_ARTIFACT_EXPERIENCE },
{"townOpenHall", EShortcut::TOWN_OPEN_HALL },
{"townOpenFort", EShortcut::TOWN_OPEN_FORT },
{"townOpenMarket", EShortcut::TOWN_OPEN_MARKET },
{"townOpenMageGuild", EShortcut::TOWN_OPEN_MAGE_GUILD },
{"townOpenThievesGuild", EShortcut::TOWN_OPEN_THIEVES_GUILD },
{"townOpenRecruitment", EShortcut::TOWN_OPEN_RECRUITMENT },
{"townOpenHeroExchange", EShortcut::TOWN_OPEN_HERO_EXCHANGE },
{"townOpenHero", EShortcut::TOWN_OPEN_HERO },
{"townOpenVisitingHero", EShortcut::TOWN_OPEN_VISITING_HERO },
{"townOpenGarrisonedHero", EShortcut::TOWN_OPEN_GARRISONED_HERO },
{"recruitmentSwitchLevel", EShortcut::RECRUITMENT_SWITCH_LEVEL },
{"heroArmySplit", EShortcut::HERO_ARMY_SPLIT },
{"heroBackpack", EShortcut::HERO_BACKPACK },
{"exchangeArmyToLeft", EShortcut::EXCHANGE_ARMY_TO_LEFT },
{"exchangeArmyToRight", EShortcut::EXCHANGE_ARMY_TO_RIGHT },
{"exchangeArmySwap", EShortcut::EXCHANGE_ARMY_SWAP },
{"exchangeArtifactsToLeft", EShortcut::EXCHANGE_ARTIFACTS_TO_LEFT },
{"exchangeArtifactsToRight", EShortcut::EXCHANGE_ARTIFACTS_TO_RIGHT },
{"exchangeArtifactsSwap", EShortcut::EXCHANGE_ARTIFACTS_SWAP },
{"exchangeBackpackLeft", EShortcut::EXCHANGE_BACKPACK_LEFT },
{"exchangeBackpackRight", EShortcut::EXCHANGE_BACKPACK_RIGHT },
{"exchangeEquippedToLeft", EShortcut::EXCHANGE_EQUIPPED_TO_LEFT },
{"exchangeEquippedToRight", EShortcut::EXCHANGE_EQUIPPED_TO_RIGHT},
{"exchangeEquippedSwap", EShortcut::EXCHANGE_EQUIPPED_SWAP },
{"exchangeBackpackToLeft", EShortcut::EXCHANGE_BACKPACK_TO_LEFT },
{"exchangeBackpackToRight", EShortcut::EXCHANGE_BACKPACK_TO_RIGHT},
{"exchangeBackpackSwap", EShortcut::EXCHANGE_BACKPACK_SWAP },
};
#ifndef ENABLE_GOLDMASTER
std::vector<EShortcut> assignedShortcuts;
std::vector<EShortcut> missingShortcuts;
for (auto const & entry : shortcutNames)
assignedShortcuts.push_back(entry.second);
for (EShortcut id = vstd::next(EShortcut::NONE, 1); id < EShortcut::AFTER_LAST; id = vstd::next(id, 1))
if (!vstd::contains(assignedShortcuts, id))
missingShortcuts.push_back(id);
if (!missingShortcuts.empty())
logGlobal->error("Found %d shortcuts without assigned string name!", missingShortcuts.size());
#endif
if (shortcutNames.count(identifier))
return shortcutNames.at(identifier);
return EShortcut::NONE;

View File

@ -71,9 +71,18 @@ CBonusSelection::CBonusSelection()
panelBackground = std::make_shared<CPicture>(ImagePath::builtin("CAMPBRF.BMP"), 456, 6);
buttonStart = std::make_shared<CButton>(Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT);
const auto & playVideo = [this]()
{
GH.windows().createAndPushWindow<CPrologEpilogVideo>(
getCampaign()->scenario(CSH->campaignMap).prolog,
[this]() { redraw(); } );
};
buttonStart = std::make_shared<CButton>(
Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT
);
buttonRestart = std::make_shared<CButton>(Point(475, 536), AnimationPath::builtin("CBRESTB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT);
buttonVideo = std::make_shared<CButton>(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), [this](){ GH.windows().createAndPushWindow<CPrologEpilogVideo>(getCampaign()->scenario(CSH->campaignMap).prolog, [this](){ redraw(); }); });
buttonVideo = std::make_shared<CButton>(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), playVideo, EShortcut::LOBBY_REPLAY_VIDEO);
buttonBack = std::make_shared<CButton>(Point(624, 536), AnimationPath::builtin("CBCANCB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL);
campaignName = std::make_shared<CLabel>(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName());
@ -104,8 +113,11 @@ CBonusSelection::CBonusSelection()
if(getCampaign()->playerSelectedDifficulty())
{
buttonDifficultyLeft = std::make_shared<CButton>(settings["general"]["enableUiEnhancements"].Bool() ? Point(693, 495) : Point(694, 508), AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this));
buttonDifficultyRight = std::make_shared<CButton>(settings["general"]["enableUiEnhancements"].Bool() ? Point(739, 495) : Point(738, 508), AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this));
Point posLeft = settings["general"]["enableUiEnhancements"].Bool() ? Point(693, 495) : Point(694, 508);
Point posRight = settings["general"]["enableUiEnhancements"].Bool() ? Point(739, 495) : Point(738, 508);
buttonDifficultyLeft = std::make_shared<CButton>(posLeft, AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this), EShortcut::MOVE_LEFT);
buttonDifficultyRight = std::make_shared<CButton>(posRight, AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this), EShortcut::MOVE_RIGHT);
}
for(auto scenarioID : getCampaign()->allScenarios())
@ -125,7 +137,7 @@ CBonusSelection::CBonusSelection()
tabExtraOptions->recActions = UPDATE | SHOWALL | LCLICK | RCLICK_POPUP;
tabExtraOptions->recreate(true);
tabExtraOptions->setEnabled(false);
buttonExtraOptions = std::make_shared<CButton>(Point(643, 431), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], [this]{ tabExtraOptions->setEnabled(!tabExtraOptions->isActive()); GH.windows().totalRedraw(); }, EShortcut::NONE);
buttonExtraOptions = std::make_shared<CButton>(Point(643, 431), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], [this]{ tabExtraOptions->setEnabled(!tabExtraOptions->isActive()); GH.windows().totalRedraw(); }, EShortcut::LOBBY_EXTRA_OPTIONS);
buttonExtraOptions->setTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.extraOptions.hover"), FONT_SMALL, Colors::WHITE);
}
}

View File

@ -57,12 +57,12 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
buttonOptions = std::make_shared<CButton>(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS);
if(settings["general"]["enableUiEnhancements"].Bool())
{
buttonTurnOptions = std::make_shared<CButton>(Point(619, 105), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE);
buttonExtraOptions = std::make_shared<CButton>(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabExtraOptions), EShortcut::NONE);
buttonTurnOptions = std::make_shared<CButton>(Point(619, 105), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::LOBBY_TURN_OPTIONS);
buttonExtraOptions = std::make_shared<CButton>(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabExtraOptions), EShortcut::LOBBY_EXTRA_OPTIONS);
}
};
buttonChat = std::make_shared<CButton>(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT);
buttonChat = std::make_shared<CButton>(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_TOGGLE_CHAT);
buttonChat->setTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE);
switch(screenType)

View File

@ -33,6 +33,7 @@
#include "../widgets/CTextInput.h"
#include "../widgets/GraphicalPrimitiveCanvas.h"
#include "../widgets/Images.h"
#include "../widgets/MiscWidgets.h"
#include "../widgets/ObjectLists.h"
#include "../widgets/Slider.h"
#include "../widgets/TextControls.h"
@ -143,8 +144,8 @@ InfoCard::InfoCard()
chat = std::make_shared<CChatBox>(Rect(18, 126, 335, 143));
pvpBox = std::make_shared<PvPBox>(Rect(17, 396, 338, 105));
buttonInvitePlayers = std::make_shared<CButton>(Point(20, 365), AnimationPath::builtin("pregameInvitePlayers"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateRoomInviteInterface(); } );
buttonOpenGlobalLobby = std::make_shared<CButton>(Point(188, 365), AnimationPath::builtin("pregameReturnToLobby"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateInterface(); });
buttonInvitePlayers = std::make_shared<CButton>(Point(20, 365), AnimationPath::builtin("pregameInvitePlayers"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateRoomInviteInterface(); }, EShortcut::LOBBY_INVITE_PLAYERS );
buttonOpenGlobalLobby = std::make_shared<CButton>(Point(188, 365), AnimationPath::builtin("pregameReturnToLobby"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateInterface(); }, EShortcut::MAIN_MENU_LOBBY );
buttonInvitePlayers->setTextOverlay (MetaString::createFromTextID("vcmi.lobby.invite.header").toString(), EFonts::FONT_SMALL, Colors::WHITE);
buttonOpenGlobalLobby->setTextOverlay(MetaString::createFromTextID("vcmi.lobby.backToLobby").toString(), EFonts::FONT_SMALL, Colors::WHITE);
@ -418,7 +419,7 @@ PvPBox::PvPBox(const Rect & rect)
LobbyPvPAction lpa;
lpa.action = LobbyPvPAction::COIN;
CSH->sendLobbyPack(lpa);
}, EShortcut::NONE);
}, EShortcut::LOBBY_FLIP_COIN);
buttonFlipCoin->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.coin.hover"), EFonts::FONT_SMALL, Colors::WHITE);
buttonRandomTown = std::make_shared<CButton>(Point(190, 31), AnimationPath::builtin("GSPBUT2.DEF"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.pvp.randomTown.help")), [getBannedTowns](){
@ -426,7 +427,7 @@ PvPBox::PvPBox(const Rect & rect)
lpa.action = LobbyPvPAction::RANDOM_TOWN;
lpa.bannedTowns = getBannedTowns();
CSH->sendLobbyPack(lpa);
}, EShortcut::NONE);
}, EShortcut::LOBBY_RANDOM_TOWN);
buttonRandomTown->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.randomTown.hover"), EFonts::FONT_SMALL, Colors::WHITE);
buttonRandomTownVs = std::make_shared<CButton>(Point(190, 56), AnimationPath::builtin("GSPBUT2.DEF"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.pvp.randomTownVs.help")), [getBannedTowns](){
@ -434,7 +435,7 @@ PvPBox::PvPBox(const Rect & rect)
lpa.action = LobbyPvPAction::RANDOM_TOWN_VS;
lpa.bannedTowns = getBannedTowns();
CSH->sendLobbyPack(lpa);
}, EShortcut::NONE);
}, EShortcut::LOBBY_RANDOM_TOWN_VS);
buttonRandomTownVs->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.randomTownVs.hover"), EFonts::FONT_SMALL, Colors::WHITE);
}

View File

@ -169,24 +169,23 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
labelMapSizes = std::make_shared<CLabel>(87, 62, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[510]);
// TODO: Global constants?
int sizes[] = {CMapHeader::MAP_SIZE_SMALL,
CMapHeader::MAP_SIZE_MIDDLE,
CMapHeader::MAP_SIZE_LARGE,
CMapHeader::MAP_SIZE_XLARGE,
0};
const char * filterIconNmes[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"};
for(int i = 0; i < 5; i++)
buttonsSortBy.push_back(std::make_shared<CButton>(Point(158 + 47 * i, 46), AnimationPath::builtin(filterIconNmes[i]), CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true)));
constexpr std::array sizes = {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_XLARGE, 0};
constexpr std::array filterIconNmes = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"};
constexpr std::array filterShortcuts = { EShortcut::MAPS_SIZE_S, EShortcut::MAPS_SIZE_M, EShortcut::MAPS_SIZE_L, EShortcut::MAPS_SIZE_XL, EShortcut::MAPS_SIZE_ALL };
int xpos[] = {23, 55, 88, 121, 306, 339};
const char * sortIconNames[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"};
for(int i = 0; i < 5; i++)
buttonsSortBy.push_back(std::make_shared<CButton>(Point(158 + 47 * i, 46), AnimationPath::builtin(filterIconNmes[i]), CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true), filterShortcuts[i]));
constexpr std::array xpos = {23, 55, 88, 121, 306, 339};
constexpr std::array sortIconNames = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"};
constexpr std::array sortShortcuts = { EShortcut::MAPS_SORT_PLAYERS, EShortcut::MAPS_SORT_SIZE, EShortcut::MAPS_SORT_FORMAT, EShortcut::MAPS_SORT_NAME, EShortcut::MAPS_SORT_VICTORY, EShortcut::MAPS_SORT_DEFEAT };
for(int i = 0; i < 6; i++)
{
ESortBy criteria = (ESortBy)i;
if(criteria == _name)
criteria = generalSortingBy;
buttonsSortBy.push_back(std::make_shared<CButton>(Point(xpos[i], 86), AnimationPath::builtin(sortIconNames[i]), CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria)));
buttonsSortBy.push_back(std::make_shared<CButton>(Point(xpos[i], 86), AnimationPath::builtin(sortIconNames[i]), CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria), sortShortcuts[i]));
}
}
@ -212,8 +211,8 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
pos.x += 3;
pos.y += 6;
buttonsSortBy.push_back(std::make_shared<CButton>(Point(23, 86), AnimationPath::builtin("CamCusM.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps)));
buttonsSortBy.push_back(std::make_shared<CButton>(Point(55, 86), AnimationPath::builtin("CamCusL.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name)));
buttonsSortBy.push_back(std::make_shared<CButton>(Point(23, 86), AnimationPath::builtin("CamCusM.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps), EShortcut::MAPS_SORT_MAPS));
buttonsSortBy.push_back(std::make_shared<CButton>(Point(55, 86), AnimationPath::builtin("CamCusL.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name), EShortcut::MAPS_SORT_NAME));
break;
default:
assert(0);
@ -222,7 +221,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
if(enableUiEnhancements)
{
auto sortByDate = std::make_shared<CButton>(Point(371, 85), AnimationPath::builtin("selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate));
auto sortByDate = std::make_shared<CButton>(Point(371, 85), AnimationPath::builtin("selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate), EShortcut::MAPS_SORT_CHANGEDATE);
sortByDate->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/selectionTabSortDate")));
buttonsSortBy.push_back(sortByDate);
}
@ -677,6 +676,8 @@ void SelectionTab::selectFileName(std::string fname)
}
}
filter(-1);
for(int i = (int)curItems.size() - 1; i >= 0; i--)
{
if(curItems[i]->fileURI == fname)
@ -687,13 +688,25 @@ void SelectionTab::selectFileName(std::string fname)
}
}
filter(-1);
selectAbs(-1);
if(tabType == ESelectionScreen::saveGame && inputName->getText().empty())
inputName->setText("NEWGAME");
}
void SelectionTab::selectNewestFile()
{
time_t newestTime = 0;
std::string newestFile = "";
for(int i = (int)allItems.size() - 1; i >= 0; i--)
if(allItems[i]->lastWrite > newestTime)
{
newestTime = allItems[i]->lastWrite;
newestFile = allItems[i]->fileURI;
}
selectFileName(newestFile);
}
std::shared_ptr<ElementInfo> SelectionTab::getSelectedMapInfo() const
{
return curItems.empty() || curItems[selectionPos]->isFolder ? nullptr : curItems[selectionPos];
@ -733,6 +746,8 @@ void SelectionTab::restoreLastSelection()
selectFileName(settings["general"]["lastCampaign"].String());
break;
case ESelectionScreen::loadGame:
selectNewestFile();
break;
case ESelectionScreen::saveGame:
selectFileName(settings["general"]["lastSave"].String());
}
@ -795,7 +810,7 @@ void SelectionTab::parseSaves(const std::unordered_set<ResourcePath> & files)
switch(CSH->getLoadMode())
{
case ELoadMode::SINGLE:
if(isMultiplayer || isCampaign || isTutorial)
if(isCampaign || isTutorial)
mapInfo->mapHeader.reset();
break;
case ELoadMode::CAMPAIGN:
@ -806,10 +821,14 @@ void SelectionTab::parseSaves(const std::unordered_set<ResourcePath> & files)
if(!isTutorial)
mapInfo->mapHeader.reset();
break;
default:
case ELoadMode::MULTI:
if(!isMultiplayer)
mapInfo->mapHeader.reset();
break;
default:
assert(0);
mapInfo->mapHeader.reset();
break;
}
allItems.push_back(mapInfo);

View File

@ -101,6 +101,7 @@ public:
int getLine() const;
int getLine(const Point & position) const;
void selectFileName(std::string fname);
void selectNewestFile();
std::shared_ptr<ElementInfo> getSelectedMapInfo() const;
void rememberCurrentSelection();
void restoreLastSelection();

View File

@ -21,6 +21,7 @@
#include "../windows/InfoWindows.h"
#include "../widgets/TextControls.h"
#include "../render/Canvas.h"
#include "../render/IRenderHandler.h"
#include "../CGameInfo.h"
#include "../CVideoHandler.h"
@ -81,7 +82,8 @@ CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted)
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
pos = center(Rect(0, 0, 800, 600));
updateShadow();
backgroundAroundMenu = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y));
addHighScores();
addButtons();
@ -109,10 +111,10 @@ void CHighScoreScreen::addButtons()
buttons.clear();
buttons.push_back(std::make_shared<CButton>(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); }));
buttons.push_back(std::make_shared<CButton>(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); }));
buttons.push_back(std::make_shared<CButton>(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); }));
buttons.push_back(std::make_shared<CButton>(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); }));
buttons.push_back(std::make_shared<CButton>(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); }, EShortcut::HIGH_SCORES_CAMPAIGNS));
buttons.push_back(std::make_shared<CButton>(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); }, EShortcut::HIGH_SCORES_SCENARIOS));
buttons.push_back(std::make_shared<CButton>(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); }, EShortcut::HIGH_SCORES_RESET));
buttons.push_back(std::make_shared<CButton>(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); }, EShortcut::GLOBAL_RETURN));
}
void CHighScoreScreen::addHighScores()
@ -222,8 +224,8 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
pos = center(Rect(0, 0, 800, 600));
updateShadow();
backgroundAroundMenu = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y));
background = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), Colors::BLACK);
if(won)
@ -289,6 +291,9 @@ int CHighScoreInputScreen::addEntry(std::string text) {
void CHighScoreInputScreen::show(Canvas & to)
{
if(background)
background->show(to);
CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false,
[&]()
{
@ -303,7 +308,11 @@ void CHighScoreInputScreen::show(Canvas & to)
else
close();
});
redraw();
if(input)
input->showAll(to);
for(auto & text : texts)
text->showAll(to);
CIntObject::show(to);
}
@ -326,6 +335,7 @@ void CHighScoreInputScreen::deactivate()
{
CCS->videoh->close();
CCS->soundh->stopSound(videoSoundHandle);
CIntObject::deactivate();
}
void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
@ -372,7 +382,8 @@ CHighScoreInput::CHighScoreInput(std::string playerName, std::function<void(std:
buttonOk = std::make_shared<CButton>(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT);
buttonCancel = std::make_shared<CButton>(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL);
statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
// FIXME: broken. Never activates?
// statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
textInput = std::make_shared<CTextInput>(Rect(18, 104, 200, 25), FONT_SMALL, ETextAlignment::CENTER, true);
textInput->setText(playerName);
}

View File

@ -15,6 +15,7 @@ class CLabel;
class CMultiLineLabel;
class CAnimImage;
class CTextInput;
class CFilledTexture;
class TransparentFilledRectangle;
@ -61,6 +62,7 @@ private:
HighScorePage highscorepage;
std::shared_ptr<CPicture> background;
std::shared_ptr<CFilledTexture> backgroundAroundMenu;
std::vector<std::shared_ptr<CButton>> buttons;
std::vector<std::shared_ptr<CLabel>> texts;
std::vector<std::shared_ptr<CAnimImage>> images;
@ -93,6 +95,7 @@ class CHighScoreInputScreen : public CWindowObject
std::vector<std::shared_ptr<CMultiLineLabel>> texts;
std::shared_ptr<CHighScoreInput> input;
std::shared_ptr<TransparentFilledRectangle> background;
std::shared_ptr<CFilledTexture> backgroundAroundMenu;
std::string video;
int videoSoundHandle;

View File

@ -108,9 +108,9 @@ void CMenuScreen::show(Canvas & to)
if(!config["video"].isNull())
{
// redraw order: background -> video -> buttons and pictures
background->redraw();
background->showAll(to);
CCS->videoh->update((int)config["video"]["x"].Float() + pos.x, (int)config["video"]["y"].Float() + pos.y, to.getInternalSurface(), true, false);
tabs->redraw();
tabs->showAll(to);
}
CIntObject::show(to);
}
@ -457,11 +457,11 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType)
playerName->setText(getPlayerName());
playerName->setCallback(std::bind(&CMultiMode::onNameChange, this, _1));
buttonHotseat = std::make_shared<CButton>(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this));
buttonLobby = std::make_shared<CButton>(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this));
buttonHotseat = std::make_shared<CButton>(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this), EShortcut::MAIN_MENU_HOTSEAT);
buttonLobby = std::make_shared<CButton>(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this), EShortcut::MAIN_MENU_LOBBY);
buttonHost = std::make_shared<CButton>(Point(373, 78 + 57 * 3), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this));
buttonJoin = std::make_shared<CButton>(Point(373, 78 + 57 * 4), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this));
buttonHost = std::make_shared<CButton>(Point(373, 78 + 57 * 3), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this), EShortcut::MAIN_MENU_HOST_GAME);
buttonJoin = std::make_shared<CButton>(Point(373, 78 + 57 * 4), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this), EShortcut::MAIN_MENU_JOIN_GAME);
buttonCancel = std::make_shared<CButton>(Point(373, 424), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL);
}

View File

@ -18,6 +18,7 @@
#include "../gui/CGuiHandler.h"
#include "../gui/FramerateManager.h"
#include "../widgets/TextControls.h"
#include "../widgets/Images.h"
#include "../render/Canvas.h"
@ -27,7 +28,8 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
addUsedEvents(LCLICK | TIME);
pos = center(Rect(0, 0, 800, 600));
updateShadow();
backgroundAroundMenu = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y));
auto audioData = CCS->videoh->getAudio(spe.prologVideo);
videoSoundHandle = CCS->soundh->playSound(audioData, -1);

View File

@ -13,6 +13,7 @@
#include "../../lib/campaign/CampaignScenarioPrologEpilog.h"
class CMultiLineLabel;
class CFilledTexture;
class CPrologEpilogVideo : public CWindowObject
{
@ -25,6 +26,7 @@ class CPrologEpilogVideo : public CWindowObject
std::function<void()> exitCb;
std::shared_ptr<CMultiLineLabel> text;
std::shared_ptr<CFilledTexture> backgroundAroundMenu;
bool voiceStopped = false;

View File

@ -664,15 +664,15 @@ size_t MapRendererPath::selectImageArrow(bool reachableToday, const int3 & curr,
// is (directionToArrowIndex[7][5])
//
const static size_t directionToArrowIndex[9][9] = {
{16, 17, 18, 7, 0, 19, 6, 5, 0 },
{8, 9, 18, 7, 0, 19, 6, 0, 20},
{8, 1, 10, 7, 0, 19, 0, 21, 20},
{24, 17, 18, 15, 0, 0, 6, 5, 4 },
{16, 17, 18, 7, 0, 19, 6, 5, 12},
{8, 9, 18, 7, 0, 19, 6, 13, 20},
{8, 1, 10, 7, 0, 19, 14, 21, 20},
{24, 17, 18, 15, 0, 11, 6, 5, 4 },
{0, 0, 0, 0, 0, 0, 0, 0, 0 },
{8, 1, 2, 0, 0, 11, 22, 21, 20},
{24, 17, 0, 23, 0, 3, 14, 5, 4 },
{24, 0, 2, 23, 0, 3, 22, 13, 4 },
{0, 1, 2, 23, 0, 3, 22, 21, 12}
{8, 1, 2, 15, 0, 11, 22, 21, 20},
{24, 17, 10, 23, 0, 3, 14, 5, 4 },
{24, 9, 2, 23, 0, 3, 22, 13, 4 },
{16, 1, 2, 23, 0, 3, 22, 21, 12}
};
size_t enterDirection = (curr.x - next.x + 1) + 3 * (curr.y - next.y + 1);

View File

@ -239,9 +239,9 @@ void MapView::onViewWorldActivated(uint32_t tileSize)
controller->setTileSize(Point(tileSize, tileSize));
}
void MapView::onMapZoomLevelChanged(int stepsChange)
void MapView::onMapZoomLevelChanged(int stepsChange, bool useDeadZone)
{
controller->modifyTileSize(stepsChange);
controller->modifyTileSize(stepsChange, useDeadZone);
}
void MapView::onViewMapActivated()

View File

@ -87,7 +87,7 @@ public:
void onViewWorldActivated(uint32_t tileSize);
/// Changes zoom level / tile size of current view by specified factor
void onMapZoomLevelChanged(int stepsChange);
void onMapZoomLevelChanged(int stepsChange, bool useDeadZone);
/// Switches view from View World mode back to standard view
void onViewMapActivated();

View File

@ -87,7 +87,7 @@ void MapViewActions::mouseMoved(const Point & cursorPosition, const Point & last
void MapViewActions::wheelScrolled(int distance)
{
adventureInt->hotkeyZoom(distance * 4);
adventureInt->hotkeyZoom(distance * 4, true);
}
void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance)
@ -114,7 +114,7 @@ void MapViewActions::gesturePinch(const Point & centerPosition, double lastUpdat
int oldZoomSteps = std::round(std::log(pinchZoomFactor) / std::log(1.01));
if (newZoomSteps != oldZoomSteps)
adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps);
adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps, true);
pinchZoomFactor = newZoom;
}

View File

@ -89,7 +89,7 @@ void MapViewController::setTileSize(const Point & tileSize)
setViewCenter(newViewCenter, model->getLevel());
}
void MapViewController::modifyTileSize(int stepsChange)
void MapViewController::modifyTileSize(int stepsChange, bool useDeadZone)
{
// we want to zoom in/out in fixed 10% steps, to allow player to return back to exactly 100% zoom just by scrolling
// so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale
@ -118,10 +118,13 @@ void MapViewController::modifyTileSize(int stepsChange)
if (actualZoom != currentZoom)
{
targetTileSize = actualZoom;
if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea)
actualZoom.x = defaultTileSize;
if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea)
actualZoom.y = defaultTileSize;
if (useDeadZone)
{
if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea)
actualZoom.x = defaultTileSize;
if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea)
actualZoom.y = defaultTileSize;
}
bool isInDeadZone = targetTileSize != actualZoom || actualZoom == Point(defaultTileSize, defaultTileSize);

View File

@ -55,7 +55,7 @@ class MapViewController : public IMapObjectObserver
private:
const int defaultTileSize = 32;
const int zoomTileDeadArea = 5;
const int zoomTileDeadArea = 4;
Point targetTileSize = Point(32, 32);
bool wasInDeadZone = true;
@ -97,7 +97,7 @@ public:
void setViewCenter(const int3 & position);
void setViewCenter(const Point & position, int level);
void setTileSize(const Point & tileSize);
void modifyTileSize(int stepsChange);
void modifyTileSize(int stepsChange, bool useDeadZone);
void tick(uint32_t timePassed);
void afterRender();

View File

@ -140,11 +140,6 @@ void CCommanderArtPlace::showPopupWindow(const Point & cursorPosition)
CArtPlace::showPopupWindow(cursorPosition);
}
CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * art)
: CArtPlace(position, art)
{
}
void CArtPlace::lockSlot(bool on)
{
if(locked == on)
@ -219,20 +214,35 @@ void CArtPlace::setGestureCallback(const ClickFunctor & callback)
gestureCallback = callback;
}
void CHeroArtPlace::addCombinedArtInfo(const std::map<const CArtifact*, int> & arts)
void CArtPlace::addCombinedArtInfo(const std::map<const ArtifactID, std::vector<ArtifactID>> & arts)
{
for(const auto & combinedArt : arts)
for(const auto & availableArts : arts)
{
std::string artList;
text += "\n\n";
text += "{" + combinedArt.first->getNameTranslated() + "}";
if(arts.size() == 1)
const auto combinedArt = availableArts.first.toArtifact();
MetaString info;
info.appendEOL();
info.appendEOL();
info.appendRawString("{");
info.appendName(combinedArt->getId());
info.appendRawString("}");
info.appendRawString(" (%d/%d)");
info.replaceNumber(availableArts.second.size());
info.replaceNumber(combinedArt->getConstituents().size());
for(const auto part : combinedArt->getConstituents())
{
for(const auto part : combinedArt.first->getConstituents())
artList += "\n" + part->getNameTranslated();
info.appendEOL();
if(vstd::contains(availableArts.second, part->getId()))
{
info.appendName(part->getId());
}
else
{
info.appendRawString("{#A9A9A9|");
info.appendName(part->getId());
info.appendRawString("}");
}
}
text += " (" + boost::str(boost::format("%d") % combinedArt.second) + " / " +
boost::str(boost::format("%d") % combinedArt.first->getConstituents().size()) + ")" + artList;
text += info.toString();
}
}

View File

@ -31,6 +31,7 @@ public:
void clickPressed(const Point & cursorPosition) override;
void showPopupWindow(const Point & cursorPosition) override;
void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
void addCombinedArtInfo(const std::map<const ArtifactID, std::vector<ArtifactID>> & arts);
private:
const CArtifactInstance * ourArt;
@ -59,13 +60,6 @@ public:
void showPopupWindow(const Point & cursorPosition) override;
};
class CHeroArtPlace: public CArtPlace
{
public:
CHeroArtPlace(Point position, const CArtifactInstance * art = nullptr);
void addCombinedArtInfo(const std::map<const CArtifact*, int> & arts);
};
namespace ArtifactUtilsClient
{
bool askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot);

View File

@ -82,7 +82,7 @@ void CArtifactsOfHeroBackpack::initAOHbackpack(size_t slots, bool slider)
const auto pos = Point(slotSizeWithMargin * (artPlaceIdx % slotsColumnsMax),
slotSizeWithMargin * (artPlaceIdx / slotsColumnsMax));
backpackSlotsBackgrounds.emplace_back(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/artifactSlotEmpty"), pos));
artPlace = std::make_shared<CHeroArtPlace>(pos);
artPlace = std::make_shared<CArtPlace>(pos);
artPlace->setArtifact(nullptr);
artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2));
artPlace->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2));

View File

@ -57,12 +57,12 @@ void CArtifactsOfHeroBase::init(
pos += position;
for(int g = 0; g < ArtifactPosition::BACKPACK_START; g++)
{
artWorn[ArtifactPosition(g)] = std::make_shared<CHeroArtPlace>(slotPos[g]);
artWorn[ArtifactPosition(g)] = std::make_shared<CArtPlace>(slotPos[g]);
}
backpack.clear();
for(int s = 0; s < 5; s++)
{
auto artPlace = std::make_shared<CHeroArtPlace>(Point(403 + 46 * s, 365));
auto artPlace = std::make_shared<CArtPlace>(Point(403 + 46 * s, 365));
backpack.push_back(artPlace);
}
for(auto artPlace : artWorn)
@ -224,21 +224,21 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit
{
artPlace->lockSlot(slotInfo->locked);
artPlace->setArtifact(slotInfo->artifact);
if(!slotInfo->artifact->isCombined())
if(slotInfo->locked || slotInfo->artifact->isCombined())
return;
// If the artifact is part of at least one combined artifact, add additional information
std::map<const ArtifactID, std::vector<ArtifactID>> arts;
for(const auto combinedArt : slotInfo->artifact->artType->getPartOf())
{
// If the artifact is part of at least one combined artifact, add additional information
std::map<const CArtifact*, int> arts;
for(const auto combinedArt : slotInfo->artifact->artType->getPartOf())
arts.try_emplace(combinedArt->getId(), std::vector<ArtifactID>{});
for(const auto part : combinedArt->getConstituents())
{
arts.insert(std::pair(combinedArt, 0));
for(const auto part : combinedArt->getConstituents())
{
if(curHero->hasArt(part->getId(), false))
arts.at(combinedArt)++;
}
if(curHero->hasArt(part->getId(), false, false, false))
arts.at(combinedArt->getId()).emplace_back(part->getId());
}
artPlace->addCombinedArtInfo(arts);
}
artPlace->addCombinedArtInfo(arts);
}
else
{

View File

@ -16,7 +16,7 @@ class CButton;
class CArtifactsOfHeroBase : virtual public CIntObject
{
protected:
using ArtPlacePtr = std::shared_ptr<CHeroArtPlace>;
using ArtPlacePtr = std::shared_ptr<CArtPlace>;
using BpackScrollFunctor = std::function<void(int)>;
public:
@ -62,7 +62,7 @@ protected:
Point(381,295) //18
};
virtual void init(const CHeroArtPlace::ClickFunctor & lClickCallback, const CHeroArtPlace::ClickFunctor & showPopupCallback,
virtual void init(const CArtPlace::ClickFunctor & lClickCallback, const CArtPlace::ClickFunctor & showPopupCallback,
const Point & position, const BpackScrollFunctor & scrollCallback);
// Assigns an artifacts to an artifact place depending on it's new slot ID
virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot);

View File

@ -42,49 +42,27 @@ void CArtifactsOfHeroMain::keyPressed(EShortcut key)
{
if(!shortcutPressed)
{
uint32_t costumeIdx;
switch(key)
int saveIdx = vstd::find_pos(costumeSaveShortcuts, key);
int loadIdx = vstd::find_pos(costumeLoadShortcuts, key);
if (saveIdx != -1)
{
case EShortcut::HERO_COSTUME_0:
costumeIdx = 0;
break;
case EShortcut::HERO_COSTUME_1:
costumeIdx = 1;
break;
case EShortcut::HERO_COSTUME_2:
costumeIdx = 2;
break;
case EShortcut::HERO_COSTUME_3:
costumeIdx = 3;
break;
case EShortcut::HERO_COSTUME_4:
costumeIdx = 4;
break;
case EShortcut::HERO_COSTUME_5:
costumeIdx = 5;
break;
case EShortcut::HERO_COSTUME_6:
costumeIdx = 6;
break;
case EShortcut::HERO_COSTUME_7:
costumeIdx = 7;
break;
case EShortcut::HERO_COSTUME_8:
costumeIdx = 8;
break;
case EShortcut::HERO_COSTUME_9:
costumeIdx = 9;
break;
default:
shortcutPressed = true;
LOCPLINT->cb->manageHeroCostume(getHero()->id, saveIdx, true);
return;
}
if (loadIdx != -1)
{
shortcutPressed = true;
LOCPLINT->cb->manageHeroCostume(getHero()->id, loadIdx, false);
return;
}
shortcutPressed = true;
LOCPLINT->cb->manageHeroCostume(getHero()->id, costumeIdx, GH.isKeyboardCtrlDown());
}
}
void CArtifactsOfHeroMain::keyReleased(EShortcut key)
{
if(vstd::contains(costumesSwitcherHotkeys, key))
if(vstd::contains(costumeSaveShortcuts, key) || vstd::contains(costumeLoadShortcuts, key))
shortcutPressed = false;
}

View File

@ -23,17 +23,29 @@ public:
void keyReleased(EShortcut key) override;
private:
const std::vector<EShortcut> costumesSwitcherHotkeys =
{
EShortcut::HERO_COSTUME_0,
EShortcut::HERO_COSTUME_1,
EShortcut::HERO_COSTUME_2,
EShortcut::HERO_COSTUME_3,
EShortcut::HERO_COSTUME_4,
EShortcut::HERO_COSTUME_5,
EShortcut::HERO_COSTUME_6,
EShortcut::HERO_COSTUME_7,
EShortcut::HERO_COSTUME_8,
EShortcut::HERO_COSTUME_9
static constexpr std::array costumeSaveShortcuts = {
EShortcut::HERO_COSTUME_SAVE_0,
EShortcut::HERO_COSTUME_SAVE_1,
EShortcut::HERO_COSTUME_SAVE_2,
EShortcut::HERO_COSTUME_SAVE_3,
EShortcut::HERO_COSTUME_SAVE_4,
EShortcut::HERO_COSTUME_SAVE_5,
EShortcut::HERO_COSTUME_SAVE_6,
EShortcut::HERO_COSTUME_SAVE_7,
EShortcut::HERO_COSTUME_SAVE_8,
EShortcut::HERO_COSTUME_SAVE_9
};
static constexpr std::array costumeLoadShortcuts = {
EShortcut::HERO_COSTUME_LOAD_0,
EShortcut::HERO_COSTUME_LOAD_1,
EShortcut::HERO_COSTUME_LOAD_2,
EShortcut::HERO_COSTUME_LOAD_3,
EShortcut::HERO_COSTUME_LOAD_4,
EShortcut::HERO_COSTUME_LOAD_5,
EShortcut::HERO_COSTUME_LOAD_6,
EShortcut::HERO_COSTUME_LOAD_7,
EShortcut::HERO_COSTUME_LOAD_8,
EShortcut::HERO_COSTUME_LOAD_9
};
};

View File

@ -19,6 +19,7 @@
#include "../render/IImage.h"
#include "../render/Graphics.h"
#include "../windows/CCreatureWindow.h"
#include "../windows/CWindowWithArtifacts.h"
#include "../windows/GUIClasses.h"
#include "../CGameInfo.h"
#include "../CPlayerInterface.h"

View File

@ -12,6 +12,7 @@
#include "CAltarArtifacts.h"
#include "../../gui/CGuiHandler.h"
#include "../../gui/Shortcut.h"
#include "../../widgets/Buttons.h"
#include "../../widgets/TextControls.h"
@ -36,16 +37,16 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance *
altarArtifacts = altarObj;
deal = std::make_shared<CButton>(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"),
CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); });
CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); }, EShortcut::MARKET_DEAL);
labels.emplace_back(std::make_shared<CLabel>(450, 32, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477]));
labels.emplace_back(std::make_shared<CLabel>(302, 424, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478]));
sacrificeAllButton = std::make_shared<CButton>(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"),
CGI->generaltexth->zelp[571], std::bind(&CExperienceAltar::sacrificeAll, this));
CGI->generaltexth->zelp[571], std::bind(&CExperienceAltar::sacrificeAll, this), EShortcut::MARKET_SACRIFICE_ALL);
sacrificeAllButton->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty());
sacrificeBackpackButton = std::make_shared<CButton>(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"),
CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this));
CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this), EShortcut::MARKET_SACRIFICE_BACKPACK);
sacrificeBackpackButton->block(hero->artifactsInBackpack.empty());
// Hero's artifacts

View File

@ -12,6 +12,7 @@
#include "CAltarCreatures.h"
#include "../../gui/CGuiHandler.h"
#include "../../gui/Shortcut.h"
#include "../../widgets/Buttons.h"
#include "../../widgets/TextControls.h"
@ -33,7 +34,7 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance *
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("ALTSACR.DEF"),
CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();});
CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();}, EShortcut::MARKET_DEAL);
labels.emplace_back(std::make_shared<CLabel>(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW,
boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
labels.emplace_back(std::make_shared<CLabel>(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479]));
@ -44,7 +45,7 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance *
unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0);
expPerUnit.resize(GameConstants::ARMY_SIZE, 0);
sacrificeAllButton = std::make_shared<CButton>(
Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CExperienceAltar::sacrificeAll, this));
Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CExperienceAltar::sacrificeAll, this), EShortcut::MARKET_SACRIFICE_ALL);
// Hero creatures panel
assert(bidTradePanel);

View File

@ -12,6 +12,7 @@
#include "CArtifactsBuying.h"
#include "../../gui/CGuiHandler.h"
#include "../../gui/Shortcut.h"
#include "../../widgets/Buttons.h"
#include "../../widgets/TextControls.h"
@ -38,7 +39,7 @@ CArtifactsBuying::CArtifactsBuying(const IMarket * market, const CGHeroInstance
title = CGI->generaltexth->allTexts[349];
labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title));
deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("TPMRKB.DEF"),
CGI->generaltexth->zelp[595], [this](){CArtifactsBuying::makeDeal();});
CGI->generaltexth->zelp[595], [this](){CArtifactsBuying::makeDeal();}, EShortcut::MARKET_DEAL);
labels.emplace_back(std::make_shared<CLabel>(445, 148, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[168]));
// Player's resources

View File

@ -12,6 +12,7 @@
#include "CArtifactsSelling.h"
#include "../../gui/CGuiHandler.h"
#include "../../gui/Shortcut.h"
#include "../../widgets/Buttons.h"
#include "../../widgets/TextControls.h"
@ -43,7 +44,7 @@ CArtifactsSelling::CArtifactsSelling(const IMarket * market, const CGHeroInstanc
labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title));
labels.push_back(std::make_shared<CLabel>(155, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[271]) % hero->getNameTranslated())));
deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("TPMRKB.DEF"),
CGI->generaltexth->zelp[595], [this](){CArtifactsSelling::makeDeal();});
CGI->generaltexth->zelp[595], [this](){CArtifactsSelling::makeDeal();}, EShortcut::MARKET_DEAL);
bidSelectedSlot = std::make_shared<CTradeableItem>(Rect(Point(123, 470), Point(69, 66)), EType::ARTIFACT_TYPE, 0, 0);
// Market resources panel

View File

@ -12,6 +12,7 @@
#include "CFreelancerGuild.h"
#include "../../gui/CGuiHandler.h"
#include "../../gui/Shortcut.h"
#include "../../widgets/Buttons.h"
#include "../../widgets/TextControls.h"
@ -39,7 +40,7 @@ CFreelancerGuild::CFreelancerGuild(const IMarket * market, const CGHeroInstance
labels.emplace_back(std::make_shared<CLabel>(155, 103, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE,
boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
CGI->generaltexth->zelp[595], [this]() {CFreelancerGuild::makeDeal();});
CGI->generaltexth->zelp[595], [this]() {CFreelancerGuild::makeDeal();}, EShortcut::MARKET_DEAL);
offerSlider->moveTo(pos.topLeft() + Point(232, 489));
// Hero creatures panel

View File

@ -14,6 +14,7 @@
#include "../Images.h"
#include "../../gui/CGuiHandler.h"
#include "../../gui/Shortcut.h"
#include "../../widgets/Buttons.h"
#include "../../widgets/TextControls.h"
@ -200,7 +201,7 @@ CMarketSlider::CMarketSlider(const CSlider::SliderMovingFunctor & movingCallback
[this]()
{
offerSlider->scrollToMax();
});
}, EShortcut::MARKET_MAX_AMOUNT);
}
void CMarketSlider::deselect()

View File

@ -12,6 +12,7 @@
#include "CMarketResources.h"
#include "../../gui/CGuiHandler.h"
#include "../../gui/Shortcut.h"
#include "../../widgets/Buttons.h"
#include "../../widgets/TextControls.h"
@ -36,7 +37,7 @@ CMarketResources::CMarketResources(const IMarket * market, const CGHeroInstance
labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158]));
deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
CGI->generaltexth->zelp[595], [this]() {CMarketResources::makeDeal(); });
CGI->generaltexth->zelp[595], [this]() {CMarketResources::makeDeal(); }, EShortcut::MARKET_DEAL);
// Player's resources
assert(bidTradePanel);

View File

@ -12,6 +12,7 @@
#include "CTransferResources.h"
#include "../../gui/CGuiHandler.h"
#include "../../gui/Shortcut.h"
#include "../../widgets/Buttons.h"
#include "../../widgets/TextControls.h"
@ -34,7 +35,7 @@ CTransferResources::CTransferResources(const IMarket * market, const CGHeroInsta
labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158]));
labels.emplace_back(std::make_shared<CLabel>(445, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[169]));
deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
CGI->generaltexth->zelp[595], [this](){CTransferResources::makeDeal();});
CGI->generaltexth->zelp[595], [this](){CTransferResources::makeDeal();}, EShortcut::MARKET_DEAL);
// Player's resources
assert(bidTradePanel);

View File

@ -1034,6 +1034,40 @@ void CCastleBuildings::openTownHall()
GH.windows().createAndPushWindow<CHallInterface>(town);
}
void CCastleBuildings::enterAnyThievesGuild()
{
std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
for(auto & town : towns)
{
if(town->builtBuildings.count(BuildingID::TAVERN))
{
LOCPLINT->showThievesGuildWindow(town);
return;
}
}
LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithTavern"));
}
void CCastleBuildings::enterAnyMarket()
{
if(town->builtBuildings.count(BuildingID::MARKETPLACE))
{
GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
return;
}
std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
for(auto & town : towns)
{
if(town->builtBuildings.count(BuildingID::MARKETPLACE))
{
GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
return;
}
}
LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
}
CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact, bool _showAvailable):
town(Town),
level(Level),
@ -1215,7 +1249,7 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst
exit = std::make_shared<CButton>(Point(744, 544), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN);
exit->setImageOrder(4, 5, 6, 7);
auto split = std::make_shared<CButton>(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [this]() { garr->splitClick(); });
auto split = std::make_shared<CButton>(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [this]() { garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT);
garr->addSplitBtn(split);
Rect barRect(9, 182, 732, 18);
@ -1224,8 +1258,8 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst
resdatabar = std::make_shared<CResDataBar>(ImagePath::builtin("ARESBAR"), 3, 575, 37, 3, 84, 78);
townlist = std::make_shared<CTownList>(3, Rect(Point(743, 414), Point(48, 128)), Point(1,16), Point(0, 32), LOCPLINT->localState->getOwnedTowns().size() );
townlist->setScrollUpButton( std::make_shared<CButton>( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306")));
townlist->setScrollDownButton( std::make_shared<CButton>( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307")));
townlist->setScrollUpButton( std::make_shared<CButton>( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306"), 0, EShortcut::MOVE_UP));
townlist->setScrollDownButton( std::make_shared<CButton>( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307"), 0, EShortcut::MOVE_DOWN));
if(from)
townlist->select(from);
@ -1326,27 +1360,14 @@ void CCastleInterface::recreateIcons()
hall = std::make_shared<CTownInfo>(80, 413, town, true);
fort = std::make_shared<CTownInfo>(122, 413, town, false);
fastTownHall = std::make_shared<CButton>(Point(80, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterTownHall(); });
fastTownHall = std::make_shared<CButton>(Point(80, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterTownHall(); }, EShortcut::TOWN_OPEN_HALL);
fastTownHall->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("ITMTL"), town->hallLevel()));
int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1;
fastArmyPurchase = std::make_shared<CButton>(Point(122, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterToTheQuickRecruitmentWindow(); });
fastArmyPurchase = std::make_shared<CButton>(Point(122, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterToTheQuickRecruitmentWindow(); }, EShortcut::TOWN_OPEN_RECRUITMENT);
fastArmyPurchase->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("itmcl"), imageIndex));
fastMarket = std::make_shared<LRClickableArea>(Rect(163, 410, 64, 42), [&]()
{
std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
for(auto & town : towns)
{
if(town->builtBuildings.count(BuildingID::MARKETPLACE))
{
GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
return;
}
}
LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
});
fastMarket = std::make_shared<LRClickableArea>(Rect(163, 410, 64, 42), [this]() { builds->enterAnyMarket(); });
fastTavern = std::make_shared<LRClickableArea>(Rect(15, 387, 58, 64), [&]()
{
if(town->builtBuildings.count(BuildingID::TAVERN))
@ -1367,18 +1388,41 @@ void CCastleInterface::recreateIcons()
for(size_t i=0; i<4; i++)
creainfo.push_back(std::make_shared<CCreaInfo>(Point(14 + 55 * (int)i, 507), town, (int)i + 4, compactCreatureInfo, useAvailableCreaturesForLabel));
}
void CCastleInterface::keyPressed(EShortcut key)
{
switch(key)
{
case EShortcut::MOVE_UP:
townlist->selectPrev();
case EShortcut::TOWN_OPEN_FORT:
GH.windows().createAndPushWindow<CFortScreen>(town);
break;
case EShortcut::MOVE_DOWN:
townlist->selectNext();
case EShortcut::TOWN_OPEN_MARKET:
builds->enterAnyMarket();
break;
case EShortcut::TOWN_OPEN_MAGE_GUILD:
if(town->hasBuilt(BuildingID::MAGES_GUILD_1))
builds->enterMagesGuild();
break;
case EShortcut::TOWN_OPEN_THIEVES_GUILD:
break;
case EShortcut::TOWN_OPEN_HERO_EXCHANGE:
if (town->visitingHero && town->garrisonHero)
LOCPLINT->showHeroExchange(town->visitingHero->id, town->garrisonHero->id);
break;
case EShortcut::TOWN_OPEN_HERO:
if (town->visitingHero)
LOCPLINT->openHeroWindow(town->visitingHero);
else if (town->garrisonHero)
LOCPLINT->openHeroWindow(town->garrisonHero);
break;
case EShortcut::TOWN_OPEN_VISITING_HERO:
if (town->visitingHero)
LOCPLINT->openHeroWindow(town->visitingHero);
break;
case EShortcut::TOWN_OPEN_GARRISONED_HERO:
if (town->garrisonHero)
LOCPLINT->openHeroWindow(town->garrisonHero);
break;
case EShortcut::TOWN_SWAP_ARMIES:
heroes->swapArmies();

View File

@ -154,7 +154,6 @@ class CCastleBuildings : public CIntObject
void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages
void enterCastleGate();
void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains
void enterMagesGuild();
void openMagesGuild();
void openTownHall();
@ -168,6 +167,9 @@ public:
void enterDwelling(int level);
void enterTownHall();
void enterMagesGuild();
void enterAnyMarket();
void enterAnyThievesGuild();
void enterToTheQuickRecruitmentWindow();
void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE);

View File

@ -0,0 +1,376 @@
/*
* CExchangeWindow.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 "CExchangeWindow.h"
#include "CHeroBackpackWindow.h"
#include "../CGameInfo.h"
#include "../CPlayerInterface.h"
#include "../gui/CGuiHandler.h"
#include "../gui/CursorHandler.h"
#include "../gui/Shortcut.h"
#include "../gui/WindowHandler.h"
#include "../widgets/CGarrisonInt.h"
#include "../widgets/Images.h"
#include "../widgets/Buttons.h"
#include "../widgets/TextControls.h"
#include "../render/IRenderHandler.h"
#include "../render/CAnimation.h"
#include "../../CCallback.h"
#include "../lib/mapObjects/CGHeroInstance.h"
#include "../lib/CGeneralTextHandler.h"
#include "../lib/CHeroHandler.h"
#include "../lib/filesystem/Filesystem.h"
#include "../lib/CSkillHandler.h"
#include "../lib/TextOperations.h"
static const std::string QUICK_EXCHANGE_BG = "quick-exchange/TRADEQE";
static bool isQuickExchangeLayoutAvailable()
{
return CResourceHandler::get()->existsResource(ImagePath::builtin("SPRITES/" + QUICK_EXCHANGE_BG));
}
CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID)
: CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin(isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2")),
controller(hero1, hero2),
moveStackLeftButtons(),
moveStackRightButtons()
{
const bool qeLayout = isQuickExchangeLayoutAvailable();
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
addUsedEvents(KEYBOARD);
heroInst[0] = LOCPLINT->cb->getHero(hero1);
heroInst[1] = LOCPLINT->cb->getHero(hero2);
auto genTitle = [](const CGHeroInstance * h)
{
boost::format fmt(CGI->generaltexth->allTexts[138]);
fmt % h->getNameTranslated() % h->level % h->getClassNameTranslated();
return boost::str(fmt);
};
titles[0] = std::make_shared<CLabel>(147, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[0]));
titles[1] = std::make_shared<CLabel>(653, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[1]));
auto PSKIL32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL32"));
PSKIL32->preload();
auto SECSK32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSK32"));
for(int g = 0; g < 4; ++g)
{
if (qeLayout)
primSkillImages.push_back(std::make_shared<CAnimImage>(PSKIL32, g, Rect(389, 12 + 26 * g, 22, 22)));
else
primSkillImages.push_back(std::make_shared<CAnimImage>(PSKIL32, g, 0, 385, 19 + 36 * g));
}
for(int leftRight : {0, 1})
{
const CGHeroInstance * hero = heroInst.at(leftRight);
for(int m=0; m<GameConstants::PRIMARY_SKILLS; ++m)
primSkillValues[leftRight].push_back(std::make_shared<CLabel>(352 + (qeLayout ? 96 : 93) * leftRight, (qeLayout ? 22 : 35) + (qeLayout ? 26 : 36) * m, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE));
for(int m=0; m < hero->secSkills.size(); ++m)
secSkillIcons[leftRight].push_back(std::make_shared<CAnimImage>(SECSK32, 0, 0, 32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88));
specImages[leftRight] = std::make_shared<CAnimImage>(AnimationPath::builtin("UN32"), hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45);
expImages[leftRight] = std::make_shared<CAnimImage>(PSKIL32, 4, 0, 103 + 490 * leftRight, qeLayout ? 41 : 45);
expValues[leftRight] = std::make_shared<CLabel>(119 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
manaImages[leftRight] = std::make_shared<CAnimImage>(PSKIL32, 5, 0, 139 + 490 * leftRight, qeLayout ? 41 : 45);
manaValues[leftRight] = std::make_shared<CLabel>(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
}
artifs[0] = std::make_shared<CArtifactsOfHeroMain>(Point(-334, 151));
artifs[0]->setHero(heroInst[0]);
artifs[1] = std::make_shared<CArtifactsOfHeroMain>(Point(98, 151));
artifs[1]->setHero(heroInst[1]);
addSetAndCallbacks(artifs[0]);
addSetAndCallbacks(artifs[1]);
for(int g=0; g<4; ++g)
{
primSkillAreas.push_back(std::make_shared<LRClickableAreaWTextComp>());
if (qeLayout)
primSkillAreas[g]->pos = Rect(Point(pos.x + 324, pos.y + 12 + 26 * g), Point(152, 22));
else
primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32));
primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g];
primSkillAreas[g]->component = Component( ComponentType::PRIM_SKILL, PrimarySkill(g));
primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1];
boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]);
}
//heroes related thing
for(int b=0; b < heroInst.size(); b++)
{
const CGHeroInstance * hero = heroInst.at(b);
//secondary skill's clickable areas
for(int g=0; g<hero->secSkills.size(); ++g)
{
SecondarySkill skill = hero->secSkills[g].first;
int level = hero->secSkills[g].second; // <1, 3>
secSkillAreas[b].push_back(std::make_shared<LRClickableAreaWTextComp>());
secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) );
secSkillAreas[b][g]->component = Component(ComponentType::SEC_SKILL, skill, level);
secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level);
secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21];
boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->generaltexth->levels[level - 1]);
boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->skillh->getByIndex(skill)->getNameTranslated());
}
heroAreas[b] = std::make_shared<CHeroArea>(257 + 228 * b, 13, hero);
heroAreas[b]->addClickCallback([this, hero]() -> void
{
if(getPickedArtifact() == nullptr)
LOCPLINT->openHeroWindow(hero);
});
specialtyAreas[b] = std::make_shared<LRClickableAreaWText>();
specialtyAreas[b]->pos = Rect(Point(pos.x + 69 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
specialtyAreas[b]->hoverText = CGI->generaltexth->heroscrn[27];
specialtyAreas[b]->text = hero->type->getSpecialtyDescriptionTranslated();
experienceAreas[b] = std::make_shared<LRClickableAreaWText>();
experienceAreas[b]->pos = Rect(Point(pos.x + 105 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
experienceAreas[b]->hoverText = CGI->generaltexth->heroscrn[9];
experienceAreas[b]->text = CGI->generaltexth->allTexts[2];
boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->level));
boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(CGI->heroh->reqExp(hero->level+1)));
boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->exp));
spellPointsAreas[b] = std::make_shared<LRClickableAreaWText>();
spellPointsAreas[b]->pos = Rect(Point(pos.x + 141 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
spellPointsAreas[b]->hoverText = CGI->generaltexth->heroscrn[22];
spellPointsAreas[b]->text = CGI->generaltexth->allTexts[205];
boost::algorithm::replace_first(spellPointsAreas[b]->text, "%s", hero->getNameTranslated());
boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->mana));
boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->manaLimit()));
morale[b] = std::make_shared<MoraleLuckBox>(true, Rect(Point(176 + 490 * b, 39), Point(32, 32)), true);
luck[b] = std::make_shared<MoraleLuckBox>(false, Rect(Point(212 + 490 * b, 39), Point(32, 32)), true);
}
quit = std::make_shared<CButton>(Point(732, 567), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT);
if(queryID.getNum() > 0)
quit->addCallback([=](){ LOCPLINT->cb->selectionMade(0, queryID); });
questlogButton[0] = std::make_shared<CButton>(Point( 10, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questLogShortcut, this), EShortcut::ADVENTURE_QUEST_LOG);
questlogButton[1] = std::make_shared<CButton>(Point(740, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questLogShortcut, this), EShortcut::ADVENTURE_QUEST_LOG);
Rect barRect(5, 578, 725, 18);
statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), barRect, 5, 578));
//garrison interface
garr = std::make_shared<CGarrisonInt>(Point(69, qeLayout ? 122 : 131), 4, Point(418,0), heroInst[0], heroInst[1], true, true);
auto splitButtonCallback = [&](){ garr->splitClick(); };
garr->addSplitBtn(std::make_shared<CButton>( Point( 10, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback, EShortcut::HERO_ARMY_SPLIT));
garr->addSplitBtn(std::make_shared<CButton>( Point(744, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback, EShortcut::HERO_ARMY_SPLIT));
if(qeLayout)
{
moveAllGarrButtonLeft = std::make_shared<CButton>(Point(325, 118), AnimationPath::builtin("quick-exchange/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
[this](){ this->moveUnitsShortcut(false); });
exchangeGarrButton = std::make_shared<CButton>(Point(377, 118), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]),
[this](){ controller.swapArmy(); });
moveAllGarrButtonRight = std::make_shared<CButton>(Point(425, 118), AnimationPath::builtin("quick-exchange/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
[this](){ this->moveUnitsShortcut(true); });
moveArtifactsButtonLeft = std::make_shared<CButton>(Point(325, 154), AnimationPath::builtin("quick-exchange/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]),
[this](){ this->moveArtifactsCallback(false);});
exchangeArtifactsButton = std::make_shared<CButton>(Point(377, 154), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]),
[this](){ this->swapArtifactsCallback(); });
moveArtifactsButtonRight = std::make_shared<CButton>(Point(425, 154), AnimationPath::builtin("quick-exchange/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]),
[this](){ this->moveArtifactsCallback(true);});
backpackButtonLeft = std::make_shared<CButton>(Point(325, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
[this](){ this->backpackShortcut(true); });
backpackButtonRight = std::make_shared<CButton>(Point(419, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
[this](){ this->backpackShortcut(false); });
backpackButtonLeft->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
backpackButtonRight->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
auto leftHeroBlock = heroInst[0]->tempOwner != LOCPLINT->cb->getPlayerID();
auto rightHeroBlock = heroInst[1]->tempOwner != LOCPLINT->cb->getPlayerID();
moveAllGarrButtonLeft->block(leftHeroBlock);
exchangeGarrButton->block(leftHeroBlock || rightHeroBlock);
moveAllGarrButtonRight->block(rightHeroBlock);
moveArtifactsButtonLeft->block(leftHeroBlock);
exchangeArtifactsButton->block(leftHeroBlock || rightHeroBlock);
moveArtifactsButtonRight->block(rightHeroBlock);
backpackButtonLeft->block(leftHeroBlock);
backpackButtonRight->block(rightHeroBlock);
for(int i = 0; i < GameConstants::ARMY_SIZE; i++)
{
moveStackLeftButtons.push_back(
std::make_shared<CButton>(
Point(484 + 35 * i, 154),
AnimationPath::builtin("quick-exchange/unitLeft.DEF"),
CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i))));
moveStackLeftButtons.back()->block(leftHeroBlock);
moveStackRightButtons.push_back(
std::make_shared<CButton>(
Point(66 + 35 * i, 154),
AnimationPath::builtin("quick-exchange/unitRight.DEF"),
CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i))));
moveStackLeftButtons.back()->block(rightHeroBlock);
}
}
updateWidgets();
}
void CExchangeWindow::moveArtifactsCallback(bool leftToRight)
{
bool moveEquipped = !GH.isKeyboardShiftDown();
bool moveBackpack = !GH.isKeyboardCmdDown();
controller.moveArtifacts(leftToRight, moveEquipped, moveBackpack);
};
void CExchangeWindow::swapArtifactsCallback()
{
bool moveEquipped = !GH.isKeyboardShiftDown();
bool moveBackpack = !GH.isKeyboardCmdDown();
controller.swapArtifacts(moveEquipped, moveBackpack);
}
void CExchangeWindow::moveUnitsShortcut(bool leftToRight)
{
std::optional<SlotID> slotId = std::nullopt;
if(const auto * slot = getSelectedSlotID())
slotId = slot->getSlot();
controller.moveArmy(leftToRight, slotId);
};
void CExchangeWindow::backpackShortcut(bool leftHero)
{
GH.windows().createAndPushWindow<CHeroBackpackWindow>(heroInst[leftHero ? 0 : 1], artSets);
};
void CExchangeWindow::keyPressed(EShortcut key)
{
switch (key)
{
case EShortcut::EXCHANGE_ARMY_TO_LEFT:
moveUnitsShortcut(false);
break;
case EShortcut::EXCHANGE_ARMY_TO_RIGHT:
moveUnitsShortcut(true);
break;
case EShortcut::EXCHANGE_ARMY_SWAP:
controller.swapArmy();
break;
case EShortcut::EXCHANGE_ARTIFACTS_TO_LEFT:
controller.moveArtifacts(false, true, true);
break;
case EShortcut::EXCHANGE_ARTIFACTS_TO_RIGHT:
controller.moveArtifacts(true, true, true);
break;
case EShortcut::EXCHANGE_ARTIFACTS_SWAP:
controller.swapArtifacts(true, true);
break;
case EShortcut::EXCHANGE_EQUIPPED_TO_LEFT:
controller.moveArtifacts(false, true, false);
break;
case EShortcut::EXCHANGE_EQUIPPED_TO_RIGHT:
controller.moveArtifacts(true, true, false);
break;
case EShortcut::EXCHANGE_EQUIPPED_SWAP:
controller.swapArtifacts(true, false);
break;
case EShortcut::EXCHANGE_BACKPACK_TO_LEFT:
controller.moveArtifacts(false, false, true);
break;
case EShortcut::EXCHANGE_BACKPACK_TO_RIGHT:
controller.moveArtifacts(true, false, true);
break;
case EShortcut::EXCHANGE_BACKPACK_SWAP:
controller.swapArtifacts(false, true);
break;
case EShortcut::EXCHANGE_BACKPACK_LEFT:
backpackShortcut(true);
break;
case EShortcut::EXCHANGE_BACKPACK_RIGHT:
backpackShortcut(false);
break;
}
}
const CGarrisonSlot * CExchangeWindow::getSelectedSlotID() const
{
return garr->getSelection();
}
void CExchangeWindow::updateGarrisons()
{
garr->recreateSlots();
updateWidgets();
}
bool CExchangeWindow::holdsGarrison(const CArmedInstance * army)
{
return garr->upperArmy() == army || garr->lowerArmy() == army;
}
void CExchangeWindow::questLogShortcut()
{
CCS->curh->dragAndDropCursor(nullptr);
LOCPLINT->showQuestLog();
}
void CExchangeWindow::updateWidgets()
{
for(size_t leftRight : {0, 1})
{
const CGHeroInstance * hero = heroInst.at(leftRight);
for(int m=0; m<GameConstants::PRIMARY_SKILLS; ++m)
{
auto value = heroInst[leftRight]->getPrimSkillLevel(static_cast<PrimarySkill>(m));
primSkillValues[leftRight][m]->setText(std::to_string(value));
}
for(int m=0; m < hero->secSkills.size(); ++m)
{
int id = hero->secSkills[m].first;
int level = hero->secSkills[m].second;
secSkillIcons[leftRight][m]->setFrame(2 + id * 3 + level);
}
expValues[leftRight]->setText(TextOperations::formatMetric(hero->exp, 3));
manaValues[leftRight]->setText(TextOperations::formatMetric(hero->mana, 3));
morale[leftRight]->set(hero);
luck[leftRight]->set(hero);
}
}

View File

@ -0,0 +1,78 @@
/*
* CExchangeWindow.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 "CWindowWithArtifacts.h"
#include "../widgets/CExchangeController.h"
class CGarrisonSlot;
class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts
{
std::array<std::shared_ptr<CLabel>, 2> titles;
std::vector<std::shared_ptr<CAnimImage>> primSkillImages;//shared for both heroes
std::array<std::vector<std::shared_ptr<CLabel>>, 2> primSkillValues;
std::array<std::vector<std::shared_ptr<CAnimImage>>, 2> secSkillIcons;
std::array<std::shared_ptr<CAnimImage>, 2> specImages;
std::array<std::shared_ptr<CAnimImage>, 2> expImages;
std::array<std::shared_ptr<CLabel>, 2> expValues;
std::array<std::shared_ptr<CAnimImage>, 2> manaImages;
std::array<std::shared_ptr<CLabel>, 2> manaValues;
std::vector<std::shared_ptr<LRClickableAreaWTextComp>> primSkillAreas;
std::array<std::vector<std::shared_ptr<LRClickableAreaWTextComp>>, 2> secSkillAreas;
std::array<std::shared_ptr<CHeroArea>, 2> heroAreas;
std::array<std::shared_ptr<LRClickableAreaWText>, 2> specialtyAreas;
std::array<std::shared_ptr<LRClickableAreaWText>, 2> experienceAreas;
std::array<std::shared_ptr<LRClickableAreaWText>, 2> spellPointsAreas;
std::array<std::shared_ptr<MoraleLuckBox>, 2> morale;
std::array<std::shared_ptr<MoraleLuckBox>, 2> luck;
std::shared_ptr<CButton> quit;
std::array<std::shared_ptr<CButton>, 2> questlogButton;
std::shared_ptr<CGarrisonInt> garr;
std::shared_ptr<CButton> moveAllGarrButtonLeft;
std::shared_ptr<CButton> exchangeGarrButton;
std::shared_ptr<CButton> moveAllGarrButtonRight;
std::shared_ptr<CButton> moveArtifactsButtonLeft;
std::shared_ptr<CButton> exchangeArtifactsButton;
std::shared_ptr<CButton> moveArtifactsButtonRight;
std::vector<std::shared_ptr<CButton>> moveStackLeftButtons;
std::vector<std::shared_ptr<CButton>> moveStackRightButtons;
std::shared_ptr<CButton> backpackButtonLeft;
std::shared_ptr<CButton> backpackButtonRight;
CExchangeController controller;
void moveArtifactsCallback(bool leftToRight);
void swapArtifactsCallback();
void moveUnitsShortcut(bool leftToRight);
void backpackShortcut(bool leftHero);
void questLogShortcut();
std::array<const CGHeroInstance *, 2> heroInst;
std::array<std::shared_ptr<CArtifactsOfHeroMain>, 2> artifs;
const CGarrisonSlot * getSelectedSlotID() const;
public:
CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID);
void keyPressed(EShortcut key) override;
void updateWidgets();
// IGarrisonHolder impl
void updateGarrisons() override;
bool holdsGarrison(const CArmedInstance * army) override;
};

View File

@ -13,7 +13,7 @@
#include "CCreatureWindow.h"
#include "CHeroBackpackWindow.h"
#include "CKingdomInterface.h"
#include "GUIClasses.h"
#include "CExchangeWindow.h"
#include "../CGameInfo.h"
#include "../CPlayerInterface.h"
@ -22,6 +22,7 @@
#include "../gui/TextAlignment.h"
#include "../gui/Shortcut.h"
#include "../gui/WindowHandler.h"
#include "../widgets/Images.h"
#include "../widgets/MiscWidgets.h"
#include "../widgets/CComponent.h"
#include "../widgets/CGarrisonInt.h"
@ -213,7 +214,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
boost::algorithm::replace_first(helpBox, "%s", CGI->generaltexth->allTexts[43]);
garr = std::make_shared<CGarrisonInt>(Point(15, 485), 8, Point(), curHero);
auto split = std::make_shared<CButton>(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); });
auto split = std::make_shared<CButton>(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT);
garr->addSplitBtn(split);
}
if(!arts)
@ -281,9 +282,8 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
for(auto cew : GH.windows().findWindows<CExchangeWindow>())
{
for(int g=0; g < cew->heroInst.size(); ++g)
if(cew->heroInst[g] == curHero)
noDismiss = true;
if (cew->holdsGarrison(curHero))
noDismiss = true;
}
//if player only have one hero and no towns

View File

@ -866,7 +866,7 @@ class ArtSlotsTab : public CIntObject
{
public:
std::shared_ptr<CAnimImage> background;
std::vector<std::shared_ptr<CHeroArtPlace>> arts;
std::vector<std::shared_ptr<CArtPlace>> arts;
ArtSlotsTab()
{
@ -874,7 +874,7 @@ public:
background = std::make_shared<CAnimImage>(AnimationPath::builtin("OVSLOT"), 4);
pos = background->pos;
for(int i=0; i<9; i++)
arts.push_back(std::make_shared<CHeroArtPlace>(Point(269+i*48, 66)));
arts.push_back(std::make_shared<CArtPlace>(Point(269+i*48, 66)));
}
};
@ -882,7 +882,7 @@ class BackpackTab : public CIntObject
{
public:
std::shared_ptr<CAnimImage> background;
std::vector<std::shared_ptr<CHeroArtPlace>> arts;
std::vector<std::shared_ptr<CArtPlace>> arts;
std::shared_ptr<CButton> btnLeft;
std::shared_ptr<CButton> btnRight;
@ -894,7 +894,7 @@ public:
btnLeft = std::make_shared<CButton>(Point(269, 66), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), 0);
btnRight = std::make_shared<CButton>(Point(675, 66), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), 0);
for(int i=0; i<8; i++)
arts.push_back(std::make_shared<CHeroArtPlace>(Point(294+i*48, 66)));
arts.push_back(std::make_shared<CArtPlace>(Point(294+i*48, 66)));
}
};

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