Przeglądaj źródła

Support for custom cursors for adventure map spells

Ivan Savenko 3 miesięcy temu
rodzic
commit
55bd4bc8bf

+ 4 - 8
client/CPlayerInterface.cpp

@@ -1234,11 +1234,10 @@ void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus
 		return;
 
 	adventureInt->onHeroChanged(hero);
-	if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING) && !gain)
-	{
-		//recalculate paths because hero has lost bonus influencing pathfinding
-		localState->erasePath(hero);
-	}
+
+	//recalculate paths because hero has lost or gained bonus influencing pathfinding
+	if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING || bonus.type == BonusType::ROUGH_TERRAIN_DISCOUNT || bonus.type == BonusType::NO_TERRAIN_PENALTY))
+		localState->verifyPath(hero);
 }
 
 void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path )
@@ -1602,9 +1601,6 @@ void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, SpellID sp
 	if(ENGINE->windows().topWindow<CSpellWindow>())
 		ENGINE->windows().popWindows(1);
 
-	if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK)
-		localState->erasePath(caster);
-
 	auto castSoundPath = spellID.toSpell()->getCastSound();
 	if(!castSoundPath.empty())
 		ENGINE->sound().playSound(castSoundPath);

+ 6 - 23
client/adventureMap/AdventureMapInterface.cpp

@@ -612,31 +612,14 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
 
 	if(spellBeingCasted)
 	{
-		switch(spellBeingCasted->id.toEnum())
-		{
-		case SpellID::SCUTTLE_BOAT:
-			if(isValidAdventureSpellTarget(targetPosition))
-				ENGINE->cursor().set(Cursor::Map::SCUTTLE_BOAT);
-			else
-				ENGINE->cursor().set(Cursor::Map::POINTER);
-			return;
 
-		case SpellID::DIMENSION_DOOR:
-			if(isValidAdventureSpellTarget(targetPosition))
-			{
-				if(GAME->interface()->cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && GAME->interface()->cb->isTileGuardedUnchecked(targetPosition))
-					ENGINE->cursor().set(Cursor::Map::T1_ATTACK);
-				else
-					ENGINE->cursor().set(Cursor::Map::TELEPORT);
-				return;
-			}
-			else
-				ENGINE->cursor().set(Cursor::Map::POINTER);
-			return;
-		default:
+		spells::detail::ProblemImpl problem;
+
+		if(isValidAdventureSpellTarget(targetPosition))
+			ENGINE->cursor().set(spellBeingCasted->getAdventureMechanics().getCursorForTarget(GAME->interface()->cb.get(), GAME->interface()->localState->getCurrentHero(), targetPosition));
+		else
 			ENGINE->cursor().set(Cursor::Map::POINTER);
-			return;
-		}
+		return;
 	}
 
 	if(!isTargetPositionVisible)

+ 1 - 21
client/battle/BattleFieldController.cpp

@@ -117,9 +117,6 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	cellUnitMovementHighlight = ENGINE->renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
 	cellUnitMaxMovementHighlight = ENGINE->renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
 
-	attackCursors = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"), EImageBlitMode::COLORKEY);
-	spellCursors = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"), EImageBlitMode::COLORKEY);
-
 	rangedFullDamageLimitImages = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"), EImageBlitMode::COLORKEY);
 	shootingRangeLimitImages = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"), EImageBlitMode::COLORKEY);
 
@@ -861,24 +858,7 @@ void BattleFieldController::show(Canvas & to)
 	renderBattlefield(to);
 
 	if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID)
-	{
-		auto combatCursorIndex = ENGINE->cursor().get<Cursor::Combat>();
-		if (combatCursorIndex)
-		{
-			auto combatImageIndex = static_cast<size_t>(*combatCursorIndex);
-			to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - ENGINE->cursor().getPivotOffsetCombat(combatImageIndex));
-			return;
-		}
-
-		auto spellCursorIndex = ENGINE->cursor().get<Cursor::Spellcast>();
-		if (spellCursorIndex)
-		{
-			auto spellImageIndex = static_cast<size_t>(*spellCursorIndex);
-			to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - ENGINE->cursor().getPivotOffsetSpellcast());
-			return;
-		}
-
-	}
+		to.draw(ENGINE->cursor().getCurrentImage(), hexPositionAbsolute(getHoveredHex()).center() - ENGINE->cursor().getPivotOffset());
 }
 
 bool BattleFieldController::receiveEvent(const Point & position, int eventType) const

+ 0 - 3
client/battle/BattleFieldController.h

@@ -37,9 +37,6 @@ class BattleFieldController : public CIntObject
 	std::shared_ptr<CAnimation> rangedFullDamageLimitImages;
 	std::shared_ptr<CAnimation> shootingRangeLimitImages;
 
-	std::shared_ptr<CAnimation> attackCursors;
-	std::shared_ptr<CAnimation> spellCursors;
-
 	/// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack
 	std::unique_ptr<Canvas> backgroundWithHexes;
 

+ 136 - 127
client/gui/CursorHandler.cpp

@@ -21,6 +21,7 @@
 #include "../render/IRenderHandler.h"
 
 #include "../../lib/CConfigHandler.h"
+#include "../../lib/json/JsonUtils.h"
 
 std::unique_ptr<ICursor> CursorHandler::createCursor()
 {
@@ -42,8 +43,6 @@ CursorHandler::CursorHandler()
 	, showing(false)
 	, pos(0,0)
 	, dndObject(nullptr)
-	, type(Cursor::Type::ADVENTURE)
-	, frame(0)
 {
 	showType = dynamic_cast<CursorSoftware *>(cursor.get()) ? Cursor::ShowType::SOFTWARE : Cursor::ShowType::HARDWARE;
 }
@@ -52,44 +51,154 @@ CursorHandler::~CursorHandler() = default;
 
 void CursorHandler::init()
 {
-	cursors =
+	JsonNode cursorConfig = JsonUtils::assembleFromFiles("config/cursors.json");
+	std::vector<AnimationPath> animations;
+
+	for (const auto & cursorEntry : cursorConfig.Struct())
 	{
-			ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR"), EImageBlitMode::COLORKEY),
-			ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"), EImageBlitMode::COLORKEY),
-			ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT"), EImageBlitMode::COLORKEY),
-			ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"), EImageBlitMode::COLORKEY)
-	};
+		CursorParameters parameters;
+		parameters.cursorID = cursorEntry.first;
+		parameters.image = ImagePath::fromJson(cursorEntry.second["image"]);
+		parameters.animation = AnimationPath::fromJson(cursorEntry.second["animation"]);
+		parameters.animationFrameIndex = cursorEntry.second["frame"].Integer();
+		parameters.isAnimated = cursorEntry.second["animated"].Bool();
+		parameters.pivot.x = cursorEntry.second["pivotX"].Integer();
+		parameters.pivot.y = cursorEntry.second["pivotY"].Integer();
+
+		cursors.push_back(parameters);
+	}
+
+	// TODO: Preload?
 
 	set(Cursor::Map::POINTER);
 }
 
-void CursorHandler::changeGraphic(Cursor::Type type, size_t index)
+void CursorHandler::set(const std::string & index)
 {
 	assert(dndObject == nullptr);
 
-	if (type == this->type && index == this->frame)
+	if (index == currentCursorID)
 		return;
 
-	this->type = type;
-	this->frame = index;
+	currentCursorID = index;
+	currentCursorIndex = 0;
+	frameTime = 0;
+	for (size_t i = 0; i < cursors.size(); ++i)
+	{
+		if (cursors[i].cursorID == index)
+		{
+			currentCursorIndex = i;
+			break;
+		}
+	}
+
+	const auto & currentCursor = cursors.at(currentCursorIndex);
+
+	if (currentCursor.image.empty())
+	{
+		if (!loadedAnimations.count(currentCursor.animation))
+			loadedAnimations[currentCursor.animation] = ENGINE->renderHandler().loadAnimation(currentCursor.animation, EImageBlitMode::COLORKEY);
+
+		if (currentCursor.isAnimated)
+			cursorImage = loadedAnimations[currentCursor.animation]->getImage(0);
+		else
+			cursorImage = loadedAnimations[currentCursor.animation]->getImage(currentCursor.animationFrameIndex);
+	}
+	else
+	{
+		if (!loadedImages.count(currentCursor.image))
+			loadedImages[currentCursor.image] = ENGINE->renderHandler().loadImage(currentCursor.image, EImageBlitMode::COLORKEY);
+		cursorImage = loadedImages[currentCursor.image];
+	}
 
 	cursor->setImage(getCurrentImage(), getPivotOffset());
 }
 
 void CursorHandler::set(Cursor::Map index)
 {
-	changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
+	constexpr std::array mapCursorNames =
+	{
+		"mapPointer",
+		"mapHourglass",
+		"mapHero",
+		"mapTown",
+		"mapTurn1Move",
+		"mapTurn1Attack",
+		"mapTurn1Sail",
+		"mapTurn1Disembark",
+		"mapTurn1Exchange",
+		"mapTurn1Visit",
+		"mapTurn2Move",
+		"mapTurn2Attack",
+		"mapTurn2Sail",
+		"mapTurn2Disembark",
+		"mapTurn2Exchange",
+		"mapTurn2Visit",
+		"mapTurn3Move",
+		"mapTurn3Attack",
+		"mapTurn3Sail",
+		"mapTurn3Disembark",
+		"mapTurn3Exchange",
+		"mapTurn3Visit",
+		"mapTurn4Move",
+		"mapTurn4Attack",
+		"mapTurn4Sail",
+		"mapTurn4Disembark",
+		"mapTurn4Exchange",
+		"mapTurn4Visit",
+		"mapTurn1SailVisit",
+		"mapTurn2SailVisit",
+		"mapTurn3SailVisit",
+		"mapTurn4SailVisit",
+		"mapScrollNorth",
+		"mapScrollNorthEast",
+		"mapScrollEast",
+		"mapScrollSouthEast",
+		"mapScrollSouth",
+		"mapScrollSouthWest",
+		"mapScrollWest",
+		"mapScrollNorthWest",
+		"UNUSED",
+		"mapDimensionDoor",
+		"mapScuttleBoat"
+	};
+
+	set(mapCursorNames.at(static_cast<int>(index)));
 }
 
 void CursorHandler::set(Cursor::Combat index)
 {
-	changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
+	constexpr std::array combatCursorNames =
+	{
+		"combatBlocked",
+		"combatMove",
+		"combatFly",
+		"combatShoot",
+		"combatHero",
+		"combatQuery",
+		"combatPointer",
+		"combatHitNorthEast",
+		"combatHitEast",
+		"combatHitSouthEast",
+		"combatHitSouthWest",
+		"combatHitWest",
+		"combatHitNorthWest",
+		"combatHitNorth",
+		"combatHitSouth",
+		"combatShootPenalty",
+		"combatShootCatapult",
+		"combatHeal",
+		"combatSacrifice",
+		"combatTeleport"
+	};
+
+	set(combatCursorNames.at(static_cast<int>(index)));
 }
 
 void CursorHandler::set(Cursor::Spellcast index)
 {
-	//Note: this is animated cursor, ignore specified frame and only change type
-	changeGraphic(Cursor::Type::SPELLBOOK, frame);
+	//Note: this is animated cursor, ignore requested frame and only change type
+	set("castSpell");
 }
 
 void CursorHandler::dragAndDropCursor(std::shared_ptr<IImage> image)
@@ -112,114 +221,12 @@ void CursorHandler::cursorMove(const int & x, const int & y)
 	cursor->setCursorPosition(pos);
 }
 
-Point CursorHandler::getPivotOffsetMap(size_t index)
-{
-	static const std::array<Point, 43> offsets = {{
-		{  0,  0}, // POINTER          =  0,
-		{  0,  0}, // HOURGLASS        =  1,
-		{ 12, 10}, // HERO             =  2,
-		{ 12, 12}, // TOWN             =  3,
-
-		{ 15, 13}, // T1_MOVE          =  4,
-		{ 13, 13}, // T1_ATTACK        =  5,
-		{ 16, 32}, // T1_SAIL          =  6,
-		{ 13, 20}, // T1_DISEMBARK     =  7,
-		{  8,  9}, // T1_EXCHANGE      =  8,
-		{ 14, 16}, // T1_VISIT         =  9,
-
-		{ 15, 13}, // T2_MOVE          = 10,
-		{ 13, 13}, // T2_ATTACK        = 11,
-		{ 16, 32}, // T2_SAIL          = 12,
-		{ 13, 20}, // T2_DISEMBARK     = 13,
-		{  8,  9}, // T2_EXCHANGE      = 14,
-		{ 14, 16}, // T2_VISIT         = 15,
-
-		{ 15, 13}, // T3_MOVE          = 16,
-		{ 13, 13}, // T3_ATTACK        = 17,
-		{ 16, 32}, // T3_SAIL          = 18,
-		{ 13, 20}, // T3_DISEMBARK     = 19,
-		{  8,  9}, // T3_EXCHANGE      = 20,
-		{ 14, 16}, // T3_VISIT         = 21,
-
-		{ 15, 13}, // T4_MOVE          = 22,
-		{ 13, 13}, // T4_ATTACK        = 23,
-		{ 16, 32}, // T4_SAIL          = 24,
-		{ 13, 20}, // T4_DISEMBARK     = 25,
-		{  8,  9}, // T4_EXCHANGE      = 26,
-		{ 14, 16}, // T4_VISIT         = 27,
-
-		{ 16, 32}, // T1_SAIL_VISIT    = 28,
-		{ 16, 32}, // T2_SAIL_VISIT    = 29,
-		{ 16, 32}, // T3_SAIL_VISIT    = 30,
-		{ 16, 32}, // T4_SAIL_VISIT    = 31,
-
-		{  6,  1}, // SCROLL_NORTH     = 32,
-		{ 16,  2}, // SCROLL_NORTHEAST = 33,
-		{ 21,  6}, // SCROLL_EAST      = 34,
-		{ 16, 16}, // SCROLL_SOUTHEAST = 35,
-		{  6, 21}, // SCROLL_SOUTH     = 36,
-		{  1, 16}, // SCROLL_SOUTHWEST = 37,
-		{  1,  5}, // SCROLL_WEST      = 38,
-		{  2,  1}, // SCROLL_NORTHWEST = 39,
-
-		{  0,  0}, // POINTER_COPY     = 40,
-		{ 14, 16}, // TELEPORT         = 41,
-		{ 20, 20}, // SCUTTLE_BOAT     = 42
-	}};
-
-	assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor
-	assert(index < offsets.size());
-	return offsets[index] * ENGINE->screenHandler().getScalingFactor();
-}
-
-Point CursorHandler::getPivotOffsetCombat(size_t index)
-{
-	static const std::array<Point, 20> offsets = {{
-		{ 12, 12 }, // BLOCKED        = 0,
-		{ 10, 14 }, // MOVE           = 1,
-		{ 14, 14 }, // FLY            = 2,
-		{ 12, 12 }, // SHOOT          = 3,
-		{ 12, 12 }, // HERO           = 4,
-		{  8, 12 }, // QUERY          = 5,
-		{  0,  0 }, // POINTER        = 6,
-		{ 21,  0 }, // HIT_NORTHEAST  = 7,
-		{ 31,  5 }, // HIT_EAST       = 8,
-		{ 21, 21 }, // HIT_SOUTHEAST  = 9,
-		{  0, 21 }, // HIT_SOUTHWEST  = 10,
-		{  0,  5 }, // HIT_WEST       = 11,
-		{  0,  0 }, // HIT_NORTHWEST  = 12,
-		{  6,  0 }, // HIT_NORTH      = 13,
-		{  6, 31 }, // HIT_SOUTH      = 14,
-		{ 14,  0 }, // SHOOT_PENALTY  = 15,
-		{ 12, 12 }, // SHOOT_CATAPULT = 16,
-		{ 12, 12 }, // HEAL           = 17,
-		{ 12, 12 }, // SACRIFICE      = 18,
-		{ 14, 20 }, // TELEPORT       = 19
-	}};
-
-	assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor
-	assert(index < offsets.size());
-	return offsets[index] * ENGINE->screenHandler().getScalingFactor();
-}
-
-Point CursorHandler::getPivotOffsetSpellcast()
-{
-	return Point(18, 28) * ENGINE->screenHandler().getScalingFactor();
-}
-
 Point CursorHandler::getPivotOffset()
 {
 	if (dndObject)
 		return dndObject->dimensions() / 2;
 
-	switch (type) {
-	case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame);
-	case Cursor::Type::COMBAT:    return getPivotOffsetCombat(frame);
-	case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast();
-	};
-
-	assert(0);
-	return {0, 0};
+	return cursors.at(currentCursorIndex).pivot;
 }
 
 std::shared_ptr<IImage> CursorHandler::getCurrentImage()
@@ -227,15 +234,17 @@ std::shared_ptr<IImage> CursorHandler::getCurrentImage()
 	if (dndObject)
 		return dndObject;
 
-	return cursors[static_cast<size_t>(type)]->getImage(frame);
+	return cursorImage;
 }
 
-void CursorHandler::updateSpellcastCursor()
+void CursorHandler::updateAnimatedCursor()
 {
 	static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame
 
 	frameTime += ENGINE->framerate().getElapsedMilliseconds() / 1000.f;
-	size_t newFrame = frame;
+	int32_t newFrame = currentFrame;
+	const auto & animationName = cursors.at(currentCursorIndex).animation;
+	const auto & animation = loadedAnimations.at(animationName);
 
 	while (frameTime >= frameDisplayDuration)
 	{
@@ -243,12 +252,12 @@ void CursorHandler::updateSpellcastCursor()
 		newFrame++;
 	}
 
-	auto & animation = cursors.at(static_cast<size_t>(type));
-
 	while (newFrame >= animation->size())
 		newFrame -= animation->size();
 
-	changeGraphic(Cursor::Type::SPELLBOOK, newFrame);
+	currentFrame = newFrame;
+	cursorImage = animation->getImage(currentFrame);
+	cursor->setImage(getCurrentImage(), getPivotOffset());
 }
 
 void CursorHandler::render()
@@ -261,8 +270,8 @@ void CursorHandler::update()
 	if(!showing)
 		return;
 
-	if (type == Cursor::Type::SPELLBOOK)
-		updateSpellcastCursor();
+	if (cursors.at(currentCursorIndex).isAnimated)
+		updateAnimatedCursor();
 
 	cursor->update();
 }

+ 29 - 37
client/gui/CursorHandler.h

@@ -18,18 +18,18 @@ class CAnimation;
 
 namespace Cursor
 {
-	enum class Type {
+	enum class Type : int8_t {
 		ADVENTURE, // set of various cursors for adventure map
 		COMBAT,    // set of various cursors for combat
 		SPELLBOOK  // animated cursor for spellcasting
 	};
 
-	enum class ShowType {
+	enum class ShowType : int8_t {
 		SOFTWARE,
 		HARDWARE
 	};
 
-	enum class Combat {
+	enum class Combat : int8_t {
 		BLOCKED        = 0,
 		MOVE           = 1,
 		FLY            = 2,
@@ -54,7 +54,7 @@ namespace Cursor
 		COUNT
 	};
 
-	enum class Map {
+	enum class Map : int8_t {
 		POINTER          =  0,
 		HOURGLASS        =  1,
 		HERO             =  2,
@@ -102,7 +102,7 @@ namespace Cursor
 		COUNT
 	};
 
-	enum class Spellcast {
+	enum class Spellcast : int8_t {
 		SPELL = 0,
 	};
 }
@@ -110,26 +110,33 @@ namespace Cursor
 /// handles mouse cursor
 class CursorHandler final
 {
-	std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
+	struct CursorParameters
+	{
+		std::string cursorID;
+		ImagePath image;
+		AnimationPath animation;
+		Point pivot;
+		int animationFrameIndex;
+		bool isAnimated;
+	};
 
-	std::array<std::shared_ptr<CAnimation>, 4> cursors;
+	std::vector<CursorParameters> cursors;
+	std::map<AnimationPath, std::shared_ptr<CAnimation>> loadedAnimations;
+	std::map<ImagePath, std::shared_ptr<IImage>> loadedImages;
 
-	bool showing;
+	std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
+	std::shared_ptr<IImage> cursorImage; //if set, overrides currentCursor
 
 	/// Current cursor
-	Cursor::Type type;
-	Cursor::ShowType showType;
-	size_t frame;
-	float frameTime;
+	std::string currentCursorID;
 	Point pos;
+	float frameTime;
+	int32_t currentCursorIndex;
+	int32_t currentFrame;
+	Cursor::ShowType showType;
+	bool showing;
 
-	void changeGraphic(Cursor::Type type, size_t index);
-
-	Point getPivotOffset();
-
-	void updateSpellcastCursor();
-
-	std::shared_ptr<IImage> getCurrentImage();
+	void updateAnimatedCursor();
 
 	std::unique_ptr<ICursor> cursor;
 
@@ -150,25 +157,10 @@ public:
 	void set(Cursor::Map index);
 	void set(Cursor::Combat index);
 	void set(Cursor::Spellcast index);
+	void set(const std::string & index);
 
-	/// Returns current index of cursor
-	template<typename Index>
-	std::optional<Index> get()
-	{
-		bool typeValid = true;
-
-		typeValid &= (std::is_same<Index, Cursor::Map>::value       )|| type != Cursor::Type::ADVENTURE;
-		typeValid &= (std::is_same<Index, Cursor::Combat>::value    )|| type != Cursor::Type::COMBAT;
-		typeValid &= (std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK;
-
-		if (typeValid)
-			return static_cast<Index>(frame);
-		return std::nullopt;
-	}
-
-	Point getPivotOffsetSpellcast();
-	Point getPivotOffsetMap(size_t index);
-	Point getPivotOffsetCombat(size_t index);
+	std::shared_ptr<IImage> getCurrentImage();
+	Point getPivotOffset();
 
 	void render();
 	void update();

+ 66 - 0
config/cursors.json

@@ -0,0 +1,66 @@
+{
+	"mapPointer"          : { "pivotX" :  0, "pivotY" :  0, "animation" : "CRADVNTR", "frame" :  0 },
+	"mapHourglass"        : { "pivotX" :  0, "pivotY" :  0, "animation" : "CRADVNTR", "frame" :  1 },
+	"mapHero"             : { "pivotX" : 12, "pivotY" : 10, "animation" : "CRADVNTR", "frame" :  2 },
+	"mapTown"             : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRADVNTR", "frame" :  3 },
+	"mapTurn1Move"        : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" :  4 },
+	"mapTurn1Attack"      : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" :  5 },
+	"mapTurn1Sail"        : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" :  6 },
+	"mapTurn1Disembark"   : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" :  7 },
+	"mapTurn1Exchange"    : { "pivotX" :  8, "pivotY" :  9, "animation" : "CRADVNTR", "frame" :  8 },
+	"mapTurn1Visit"       : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" :  9 },
+	"mapTurn2Move"        : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 10 },
+	"mapTurn2Attack"      : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 11 },
+	"mapTurn2Sail"        : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 12 },
+	"mapTurn2Disembark"   : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 13 },
+	"mapTurn2Exchange"    : { "pivotX" :  8, "pivotY" :  9, "animation" : "CRADVNTR", "frame" : 14 },
+	"mapTurn2Visit"       : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 15 },
+	"mapTurn3Move"        : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 16 },
+	"mapTurn3Attack"      : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 17 },
+	"mapTurn3Sail"        : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 18 },
+	"mapTurn3Disembark"   : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 19 },
+	"mapTurn3Exchange"    : { "pivotX" :  8, "pivotY" :  9, "animation" : "CRADVNTR", "frame" : 20 },
+	"mapTurn3Visit"       : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 21 },
+	"mapTurn4Move"        : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 22 },
+	"mapTurn4Attack"      : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 23 },
+	"mapTurn4Sail"        : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 24 },
+	"mapTurn4Disembark"   : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 25 },
+	"mapTurn4Exchange"    : { "pivotX" :  8, "pivotY" :  9, "animation" : "CRADVNTR", "frame" : 26 },
+	"mapTurn4Visit"       : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 27 },
+	"mapTurn1SailVisit"   : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 28 },
+	"mapTurn2SailVisit"   : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 29 },
+	"mapTurn3SailVisit"   : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 30 },
+	"mapTurn4SailVisit"   : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 31 },
+	"mapScrollNorth"      : { "pivotX" :  6, "pivotY" :  1, "animation" : "CRADVNTR", "frame" : 32 },
+	"mapScrollNorthEast"  : { "pivotX" : 16, "pivotY" :  2, "animation" : "CRADVNTR", "frame" : 33 },
+	"mapScrollEast"       : { "pivotX" : 21, "pivotY" :  6, "animation" : "CRADVNTR", "frame" : 34 },
+	"mapScrollSouthEast"  : { "pivotX" : 16, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 35 },
+	"mapScrollSouth"      : { "pivotX" :  6, "pivotY" : 21, "animation" : "CRADVNTR", "frame" : 36 },
+	"mapScrollSouthWest"  : { "pivotX" :  1, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 37 },
+	"mapScrollWest"       : { "pivotX" :  1, "pivotY" :  5, "animation" : "CRADVNTR", "frame" : 38 },
+	"mapScrollNorthWest"  : { "pivotX" :  2, "pivotY" :  1, "animation" : "CRADVNTR", "frame" : 39 },
+	"mapDimensionDoor"    : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 41 },
+	"mapDimensionDoorGuarded" : { "pivotX" : 14, "pivotY" : 16, "image" : "CursrA51" },
+	"mapScuttleBoat"      : { "pivotX" : 20, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 42 },
+	"combatBlocked"       : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" :  0},
+	"combatMove"          : { "pivotX" : 10, "pivotY" : 14, "animation" : "CRCOMBAT", "frame" :  1},
+	"combatFly"           : { "pivotX" : 14, "pivotY" : 14, "animation" : "CRCOMBAT", "frame" :  2},
+	"combatShoot"         : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" :  3},
+	"combatHero"          : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" :  4},
+	"combatQuery"         : { "pivotX" :  8, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" :  5},
+	"combatPointer"       : { "pivotX" :  0, "pivotY" :  0, "animation" : "CRCOMBAT", "frame" :  6},
+	"combatHitNorthEast"  : { "pivotX" : 21, "pivotY" :  0, "animation" : "CRCOMBAT", "frame" :  7},
+	"combatHitEast"       : { "pivotX" : 31, "pivotY" :  5, "animation" : "CRCOMBAT", "frame" :  8},
+	"combatHitSouthEast"  : { "pivotX" : 21, "pivotY" : 21, "animation" : "CRCOMBAT", "frame" :  9},
+	"combatHitSouthWest"  : { "pivotX" :  0, "pivotY" : 21, "animation" : "CRCOMBAT", "frame" : 10},
+	"combatHitWest"       : { "pivotX" :  0, "pivotY" :  5, "animation" : "CRCOMBAT", "frame" : 11},
+	"combatHitNorthWest"  : { "pivotX" :  0, "pivotY" :  0, "animation" : "CRCOMBAT", "frame" : 12},
+	"combatHitNorth"      : { "pivotX" :  6, "pivotY" :  0, "animation" : "CRCOMBAT", "frame" : 13},
+	"combatHitSouth"      : { "pivotX" :  6, "pivotY" : 31, "animation" : "CRCOMBAT", "frame" : 14},
+	"combatShootPenalty"  : { "pivotX" : 14, "pivotY" :  0, "animation" : "CRCOMBAT", "frame" : 15},
+	"combatShootCatapult" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 16},
+	"combatHeal"          : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 17},
+	"combatSacrifice"     : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 18},
+	"combatTeleport"      : { "pivotX" : 14, "pivotY" : 20, "animation" : "CRCOMBAT", "frame" : 19},
+	"castSpell"           : { "pivotX" : 18, "pivotY" : 28, "animation" : "CRSPELL", "animated" : true }
+}

+ 3 - 0
config/spells/adventure.json

@@ -45,6 +45,7 @@
 				"adventureEffect" : {
 					"type" : "removeObject",
 					"castsPerDay" : 0,
+					"cursor" : "mapScuttleBoat", // defined in config/cursors.json
 					"rangeX" : 9,
 					"rangeY" : 8,
 					"ignoreFow" : false,
@@ -375,6 +376,8 @@
 					"movementPointsRequired" : 0,
 					"movementPointsTaken" : 300,
 					"waterLandFailureTakesPoints" : true,
+					"cursor" : "mapDimensionDoor", // defined in config/cursors.json
+					"cursorGuarded" : "mapTurn1Attack", // defined in config/cursors.json
 					"castsPerDay" : 2,
 					"rangeX" : 9,
 					"rangeY" : 8,

+ 5 - 0
lib/callback/EditorCallback.cpp

@@ -62,6 +62,11 @@ const TerrainTile * EditorCallback::getTileUnchecked(int3) const
 	THROW_EDITOR_UNSUPPORTED;
 }
 
+bool EditorCallback::isTileGuardedUnchecked(int3 tile) const
+{
+	THROW_EDITOR_UNSUPPORTED;
+}
+
 const CGObjectInstance * EditorCallback::getTopObj(int3) const
 {
 	THROW_EDITOR_UNSUPPORTED;

+ 1 - 0
lib/callback/EditorCallback.h

@@ -32,6 +32,7 @@ public:
 
 	const TerrainTile * getTile(int3 tile, bool verbose) const override;
 	const TerrainTile * getTileUnchecked(int3 tile) const override;
+	bool isTileGuardedUnchecked(int3 tile) const override;
 	const CGObjectInstance * getTopObj(int3 pos) const override;
 	EDiggingStatus getTileDigStatus(int3 tile, bool verbose) const override;
 	void calculatePaths(const std::shared_ptr<PathfinderConfig> & config) const override;

+ 2 - 0
lib/callback/IGameInfoCallback.h

@@ -147,6 +147,8 @@ public:
 	virtual bool checkForVisitableDir(const int3 & src, const int3 & dst) const = 0;
 	/// Returns all wandering monsters that guard specified tile
 	virtual std::vector<const CGObjectInstance *> getGuardingCreatures (int3 pos) const = 0;
+	/// Returns if tile is guarded by wandering monsters without checking whether player has access to the tile. AVOID USAGE.
+	virtual bool isTileGuardedUnchecked(int3 tile) const = 0;
 
 	/// Returns all tiles within specified range with specific tile visibility mode
 	virtual void getTilesInRange(std::unordered_set<int3> & tiles, const int3 & pos, int radius, ETileVisibility mode, std::optional<PlayerColor> player = std::optional<PlayerColor>(), int3::EDistanceFormula formula = int3::DIST_2D) const = 0;

+ 1 - 0
lib/spells/ISpellMechanics.h

@@ -355,6 +355,7 @@ public:
 	virtual bool canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const = 0;
 	virtual bool canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const = 0;
 	virtual bool isTargetInRange(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const = 0;
+	virtual std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const = 0;
 
 	virtual bool adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const = 0;
 

+ 3 - 1
lib/spells/adventure/AdventureSpellEffect.h

@@ -35,6 +35,7 @@ public:
 	virtual bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const {return true;};
 	virtual bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const {return true;};
 	virtual bool isTargetInRange(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const {return true;};
+	virtual std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const {return {};};
 };
 
 class AdventureSpellEffect final : public IAdventureSpellEffect
@@ -52,7 +53,8 @@ class AdventureSpellRangedEffect : public IAdventureSpellEffect
 public:
 	AdventureSpellRangedEffect(const JsonNode & config);
 
-	bool isTargetInRange(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override;
+	bool isTargetInRange(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
+	std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override = 0; //must be implemented in derived
 };
 
 VCMI_LIB_NAMESPACE_END

+ 5 - 0
lib/spells/adventure/AdventureSpellMechanics.cpp

@@ -130,6 +130,11 @@ bool AdventureSpellMechanics::isTargetInRange(spells::Problem & problem, const I
 	return getLevel(caster).effect->isTargetInRange(problem, cb, caster, pos);
 }
 
+std::string AdventureSpellMechanics::getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
+{
+	return getLevel(caster).effect->getCursorForTarget(cb, caster, pos);
+}
+
 bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 {
 	spells::detail::ProblemImpl problem;

+ 1 - 0
lib/spells/adventure/AdventureSpellMechanics.h

@@ -37,6 +37,7 @@ public:
 	bool canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final;
 	bool canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
 	bool isTargetInRange(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
+	std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
 	bool adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
 
 	void giveBonuses(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;

+ 16 - 0
lib/spells/adventure/DimensionDoorEffect.cpp

@@ -23,6 +23,8 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 DimensionDoorEffect::DimensionDoorEffect(const CSpell * s, const JsonNode & config)
 	: AdventureSpellRangedEffect(config)
+	, cursor(config["cursor"].String())
+	, cursorGuarded(config["cursorGuarded"].String())
 	, movementPointsRequired(config["movementPointsRequired"].Integer())
 	, movementPointsTaken(config["movementPointsTaken"].Integer())
 	, waterLandFailureTakesPoints(config["waterLandFailureTakesPoints"].Bool())
@@ -30,6 +32,20 @@ DimensionDoorEffect::DimensionDoorEffect(const CSpell * s, const JsonNode & conf
 {
 }
 
+std::string DimensionDoorEffect::getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
+{
+	if(!cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS))
+		return cursor;
+
+	if (!exposeFow && !cb->isVisibleFor(pos, caster->getCasterOwner()))
+		return cursor;
+
+	if (!cb->isTileGuardedUnchecked(pos))
+		return cursor;
+
+	return cursorGuarded;
+}
+
 bool DimensionDoorEffect::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const
 {
 	if(!caster->getHeroCaster())

+ 7 - 4
lib/spells/adventure/DimensionDoorEffect.h

@@ -16,6 +16,8 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class DimensionDoorEffect final : public AdventureSpellRangedEffect
 {
+	std::string cursor;
+	std::string cursorGuarded;
 	int movementPointsRequired;
 	int movementPointsTaken;
 	bool waterLandFailureTakesPoints;
@@ -25,11 +27,12 @@ public:
 	DimensionDoorEffect(const CSpell * s, const JsonNode & config);
 
 protected:
-	bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const override;
-	bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override;
+	bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final;
+	bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
 
-	ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
-	void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
+	ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
+	void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
+	std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
 };
 
 VCMI_LIB_NAMESPACE_END

+ 6 - 0
lib/spells/adventure/RemoveObjectEffect.cpp

@@ -25,6 +25,7 @@ RemoveObjectEffect::RemoveObjectEffect(const CSpell * s, const JsonNode & config
 	: AdventureSpellRangedEffect(config)
 	, owner(s)
 	, failMessage(MetaString::createFromTextID("core.genrltxt.337")) //%s tried to scuttle the boat, but failed
+	, cursor(config["cursor"].String())
 {
 	for(const auto & objectNode : config["objects"].Struct())
 	{
@@ -38,6 +39,11 @@ RemoveObjectEffect::RemoveObjectEffect(const CSpell * s, const JsonNode & config
 	}
 }
 
+std::string RemoveObjectEffect::getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
+{
+	return cursor;
+}
+
 bool RemoveObjectEffect::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
 {
 	if (!isTargetInRange(problem, cb, caster, pos))

+ 4 - 3
lib/spells/adventure/RemoveObjectEffect.h

@@ -19,13 +19,14 @@ class RemoveObjectEffect final : public AdventureSpellRangedEffect
 	const CSpell * owner;
 	std::vector<MapObjectID> removedObjects;
 	MetaString failMessage;
-	ImagePath cursorPath;
+	std::string cursor;
 
 public:
 	RemoveObjectEffect(const CSpell * s, const JsonNode & config);
 
-	bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override;
-	ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
+	bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
+	ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
+	std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
 };
 
 VCMI_LIB_NAMESPACE_END

+ 2 - 2
lib/spells/adventure/SummonBoatEffect.h

@@ -24,9 +24,9 @@ public:
 	SummonBoatEffect(const CSpell * s, const JsonNode & config);
 
 protected:
-	bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const override;
+	bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final;
 
-	ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
+	ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
 };
 
 VCMI_LIB_NAMESPACE_END

+ 1 - 0
test/mock/mock_IGameInfoCallback.h

@@ -78,6 +78,7 @@ public:
 	MOCK_CONST_METHOD1(guardingCreaturePosition, int3(int3 pos));
 	MOCK_CONST_METHOD2(checkForVisitableDir, bool(const int3 & src, const int3 & dst));
 	MOCK_CONST_METHOD1(getGuardingCreatures, std::vector<const CGObjectInstance *>(int3 pos));
+	MOCK_CONST_METHOD1(isTileGuardedUnchecked, bool(int3 pos));
 
 	MOCK_METHOD2(pickAllowedArtsSet, void(std::vector<ArtifactID> & out, vstd::RNG & rand));