Browse Source

Merge pull request #1932 from rilian-la-te/proper-teleport

VCMI: teleport redesign
Ivan Savenko 2 years ago
parent
commit
e3ed728193

+ 7 - 9
client/battle/BattleActionsController.cpp

@@ -569,10 +569,10 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 			return false;
 
 		case PossiblePlayerBattleAction::ANY_LOCATION:
-			return isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex);
+			return isCastingPossibleHere(action.spell().toSpell(), targetStack, targetHex);
 
 		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex);
+			return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), targetStack, targetHex);
 
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
 			if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
@@ -583,18 +583,14 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 			return false;
 
 		case PossiblePlayerBattleAction::TELEPORT:
-		{
-			ui8 skill = getCurrentSpellcaster()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell());
-			return owner.curInt->cb->battleCanTeleportTo(selectedStack, targetHex, skill);
-		}
+			return selectedStack && isCastingPossibleHere(action.spell().toSpell(), selectedStack, targetHex);
 
 		case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice
 			return targetStack && targetStack != selectedStack && targetStackOwned && targetStack->alive();
 
 		case PossiblePlayerBattleAction::OBSTACLE:
 		case PossiblePlayerBattleAction::FREE_LOCATION:
-			return isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex);
-			return isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex);
+			return isCastingPossibleHere(action.spell().toSpell(), targetStack, targetHex);
 
 		case PossiblePlayerBattleAction::CATAPULT:
 			return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex);
@@ -904,7 +900,7 @@ spells::Mode BattleActionsController::getCurrentCastMode() const
 
 }
 
-bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *casterStack, const CStack *targetStack, BattleHex targetHex)
+bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, BattleHex targetHex)
 {
 	assert(currentSpell);
 	if (!currentSpell)
@@ -915,6 +911,8 @@ bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell,
 	const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE;
 
 	spells::Target target;
+	if(targetStack)
+		target.emplace_back(targetStack);
 	target.emplace_back(targetHex);
 
 	spells::BattleCast cast(owner.curInt->cb.get(), caster, mode, currentSpell);

+ 1 - 1
client/battle/BattleActionsController.h

@@ -53,7 +53,7 @@ class BattleActionsController
 	/// stack that has been selected as first target for multi-target spells (Teleport & Sacrifice)
 	const CStack * selectedStack;
 
-	bool isCastingPossibleHere (const CSpell * spell, const CStack *sactive, const CStack *shere, BattleHex myNumber);
+	bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, BattleHex myNumber);
 	bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback
 	std::vector<PossiblePlayerBattleAction> getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn
 	void reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context);

+ 10 - 10
client/battle/BattleAnimationClasses.cpp

@@ -158,10 +158,10 @@ ECreatureAnimType AttackAnimation::findValidGroup( const std::vector<ECreatureAn
 
 const CCreature * AttackAnimation::getCreature() const
 {
-	if (attackingStack->getCreature()->getId() == CreatureID::ARROW_TOWERS)
+	if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS)
 		return owner.siegeController->getTurretCreature();
 	else
-		return attackingStack->getCreature();
+		return attackingStack->unitType();
 }
 
 
@@ -179,7 +179,7 @@ HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack)
 	: StackActionAnimation(owner, stack)
 {
 	setGroup(ECreatureAnimType::HITTED);
-	setSound(battle_sound(stack->getCreature(), wince));
+	setSound(battle_sound(stack->unitType(), wince));
 	logAnim->debug("Created HittedAnimation for %s", stack->getName());
 }
 
@@ -187,14 +187,14 @@ DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack
 	: StackActionAnimation(owner, stack)
 {
 	setGroup(ECreatureAnimType::DEFENCE);
-	setSound(battle_sound(stack->getCreature(), defend));
+	setSound(battle_sound(stack->unitType(), defend));
 	logAnim->debug("Created DefenceAnimation for %s", stack->getName());
 }
 
 DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged):
 	StackActionAnimation(owner, stack)
 {
-	setSound(battle_sound(stack->getCreature(), killed));
+	setSound(battle_sound(stack->unitType(), killed));
 
 	if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0)
 		setGroup(ECreatureAnimType::DEATH_RANGED);
@@ -356,13 +356,13 @@ bool MovementAnimation::init()
 
 	if (moveSoundHander == -1)
 	{
-		moveSoundHander = CCS->soundh->playSound(battle_sound(stack->getCreature(), move), -1);
+		moveSoundHander = CCS->soundh->playSound(battle_sound(stack->unitType(), move), -1);
 	}
 
 	Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack);
 	Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack);
 
-	progressPerSecond = AnimationControls::getMovementDistance(stack->getCreature());
+	progressPerSecond = AnimationControls::getMovementDistance(stack->unitType());
 
 	begX = begPosition.x;
 	begY = begPosition.y;
@@ -373,7 +373,7 @@ bool MovementAnimation::init()
 	if (stack->hasBonus(Selector::type()(Bonus::FLYING)))
 	{
 		float distance = static_cast<float>(sqrt(distanceX * distanceX + distanceY * distanceY));
-		progressPerSecond =  AnimationControls::getFlightDistance(stack->getCreature()) / distance;
+		progressPerSecond =  AnimationControls::getFlightDistance(stack->unitType()) / distance;
 	}
 
 	return true;
@@ -453,7 +453,7 @@ bool MovementEndAnimation::init()
 	logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName());
 	myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack));
 
-	CCS->soundh->playSound(battle_sound(stack->getCreature(), endMoving));
+	CCS->soundh->playSound(battle_sound(stack->unitType(), endMoving));
 
 	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END))
 	{
@@ -494,7 +494,7 @@ bool MovementStartAnimation::init()
 	}
 
 	logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName());
-	CCS->soundh->playSound(battle_sound(stack->getCreature(), startMoving));
+	CCS->soundh->playSound(battle_sound(stack->unitType(), startMoving));
 
 	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START))
 	{

+ 1 - 1
client/battle/BattleProjectileController.cpp

@@ -146,7 +146,7 @@ BattleProjectileController::BattleProjectileController(BattleInterface & owner):
 
 const CCreature & BattleProjectileController::getShooter(const CStack * stack) const
 {
-	const CCreature * creature = stack->getCreature();
+	const CCreature * creature = stack->unitType();
 
 	if(creature->getId() == CreatureID::ARROW_TOWERS)
 		creature = owner.siegeController->getTurretCreature();

+ 2 - 2
client/battle/BattleStacksController.cpp

@@ -49,7 +49,7 @@ static void onAnimationFinished(const CStack *stack, std::weak_ptr<CreatureAnima
 
 	if (animation->isIdle())
 	{
-		const CCreature *creature = stack->getCreature();
+		const CCreature *creature = stack->unitType();
 
 		if (stack->isFrozen())
 			animation->setType(ECreatureAnimType::FROZEN);
@@ -207,7 +207,7 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 	}
 	else
 	{
-		stackAnimation[stack->ID] = AnimationControls::getAnimation(stack->getCreature());
+		stackAnimation[stack->ID] = AnimationControls::getAnimation(stack->unitType());
 		stackAnimation[stack->ID]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->ID]);
 		stackAnimation[stack->ID]->pos.h = stackAnimation[stack->ID]->getHeight();
 		stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth();

+ 15 - 5
config/spells/other.json

@@ -140,11 +140,6 @@
 							"appearAnimation" : "C09SPF0",
 							"appearSound" : "LANDMINE"
 						}
-					},
-					"damage":{
-						"type":"core:damage",
-						"optional":false,
-						"indirect":true
 					}
 				}
 			},
@@ -641,6 +636,21 @@
 					}
 				},
 				"targetModifier":{"smart":true}
+			},
+			"advanced" :{
+				"battleEffects":{
+					"teleport":{
+						"isMoatPassable" : true
+					}
+				}
+			},
+			"expert" : {
+				"battleEffects":{
+					"teleport":{
+						"isWallPassable" : true,
+						"isMoatPassable" : true
+					}
+				}
 			}
 		},
 		"flags" : {

+ 0 - 4
include/vcmi/spells/Spell.h

@@ -53,12 +53,8 @@ public:
 	 */
 	virtual int32_t getLevelPower(const int32_t skillLevel) const = 0;
 
-	virtual std::string getNameTextID() const = 0;
-	virtual std::string getNameTranslated() const  = 0;
-
 	virtual std::string getDescriptionTextID(int32_t level) const  = 0;
 	virtual std::string getDescriptionTranslated(int32_t level) const  = 0;
-
 };
 
 }

+ 2 - 7
lib/CStack.cpp

@@ -56,11 +56,6 @@ CStack::CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I
 	health.init(); //???
 }
 
-const CCreature * CStack::getCreature() const
-{
-	return type;
-}
-
 void CStack::localInit(BattleInfo * battleInfo)
 {
 	battle = battleInfo;
@@ -88,7 +83,7 @@ ui32 CStack::level() const
 	if(base)
 		return base->getLevel(); //creature or commander
 	else
-		return std::max(1, static_cast<int>(getCreature()->getLevel())); //war machine, clone etc
+		return std::max(1, static_cast<int>(unitType()->getLevel())); //war machine, clone etc
 }
 
 si32 CStack::magicResistance() const
@@ -346,7 +341,7 @@ bool CStack::unitHasAmmoCart(const battle::Unit * unit) const
 {
 	for(const CStack * st : battle->stacks)
 	{
-		if(battle->battleMatchOwner(st, unit, true) && st->getCreature()->getId() == CreatureID::AMMO_CART)
+		if(battle->battleMatchOwner(st, unit, true) && st->unitType()->getId() == CreatureID::AMMO_CART)
 		{
 			return st->alive();
 		}

+ 0 - 2
lib/CStack.h

@@ -43,8 +43,6 @@ public:
 	CStack();
 	~CStack();
 
-	const CCreature * getCreature() const; //deprecated
-
 	std::string nodeName() const override;
 
 	void localInit(BattleInfo * battleInfo);

+ 2 - 2
lib/battle/BattleInfo.cpp

@@ -52,7 +52,7 @@ void BattleInfo::calculateCasualties(std::map<ui32,si32> * casualties) const
 		const CStack * const st = elem;
 		si32 killed = st->getKilled();
 		if(killed > 0)
-			casualties[st->side][st->getCreature()->getId()] += killed;
+			casualties[st->side][st->unitType()->getId()] += killed;
 	}
 }
 
@@ -210,7 +210,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	if(town)
 	{
 		curB->town = town;
-		curB->terrainType = (*VLC->townh)[town->subID]->nativeTerrain;
+		curB->terrainType = town->getNativeTerrain();
 	}
 	else
 	{

+ 115 - 48
lib/battle/CBattleInfoCallback.cpp

@@ -14,9 +14,13 @@
 
 #include "../CStack.h"
 #include "BattleInfo.h"
+#include "CObstacleInstance.h"
 #include "DamageCalculator.h"
 #include "PossiblePlayerBattleAction.h"
 #include "../NetPacks.h"
+#include "../spells/ObstacleCasterProxy.h"
+#include "../spells/ISpellMechanics.h"
+#include "../spells/Problem.h"
 #include "../spells/CSpellHandler.h"
 #include "../mapObjects/CGTownInstance.h"
 #include "../BattleFieldHandler.h"
@@ -133,11 +137,13 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(con
 	return ESpellCastProblem::OK;
 }
 
-bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const
+bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const
 {
 	auto isTileBlocked = [&](BattleHex tile)
 	{
 		EWallPart wallPart = battleHexToWallPart(tile);
+		if (wallPart == EWallPart::INVALID)
+			return false; // there is no wall here
 		if (wallPart == EWallPart::INDESTRUCTIBLE_PART_OF_GATE)
 			return false; // does not blocks ranged attacks
 		if (wallPart == EWallPart::INDESTRUCTIBLE_PART)
@@ -145,65 +151,67 @@ bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, Bat
 
 		return isWallPartAttackable(wallPart);
 	};
-
-	auto needWallPenalty = [&](BattleHex from, BattleHex dest)
+	// Count wall penalty requirement by shortest path, not by arbitrary line, to avoid various OH3 bugs
+	auto getShortestPath = [](BattleHex from, BattleHex dest) -> std::vector<BattleHex>
 	{
-		// arbitrary selected cell size for virtual grid
-		// any even number can be selected (for division by two)
-		static const int cellSize = 10;
+		//Out early
+		if(from == dest)
+			return {};
 
-		// create line that goes from center of shooter cell to center of target cell
-		Point line1{ from.getX()*cellSize+cellSize/2, from.getY()*cellSize+cellSize/2};
-		Point line2{ dest.getX()*cellSize+cellSize/2, dest.getY()*cellSize+cellSize/2};
+		std::vector<BattleHex> ret;
+		auto next = from;
+		//Not a real direction, only to indicate to which side we should search closest tile
+		auto direction = from.getX() > dest.getX() ? BattleSide::DEFENDER : BattleSide::ATTACKER;
 
-		for (int y = 0; y < GameConstants::BFIELD_HEIGHT; ++y)
+		while (next != dest)
 		{
-			BattleHex obstacle = lineToWallHex(y);
-			if (!isTileBlocked(obstacle))
-				continue;
-
-			// create rect around cell with an obstacle
-			Rect rect {
-				Point(obstacle.getX(), obstacle.getY()) * cellSize,
-				Point( cellSize, cellSize)
-			};
-
-			if ( rect.intersectionTest(line1, line2))
-				return true;
+			auto tiles = next.neighbouringTiles();
+			std::set<BattleHex> possibilities = {tiles.begin(), tiles.end()};
+			next = BattleHex::getClosestTile(direction, dest, possibilities);
+			ret.push_back(next);
 		}
-		return false;
+		assert(!ret.empty());
+		ret.pop_back(); //Remove destination hex
+		return ret;
 	};
 
 	RETURN_IF_NOT_BATTLE(false);
-	if(!battleGetSiegeLevel())
-		return false;
+	auto checkNeeded = !sameSideOfWall(from, dest);
+	bool pathHasWall = false;
+	bool pathHasMoat = false;
 
-	const std::string cachingStrNoWallPenalty = "type_NO_WALL_PENALTY";
-	static const auto selectorNoWallPenalty = Selector::type()(Bonus::NO_WALL_PENALTY);
+	for(const auto & hex : getShortestPath(from, dest))
+	{
+		pathHasWall |= isTileBlocked(hex);
+		if(!checkMoat)
+			continue;
 
-	if(shooter->hasBonus(selectorNoWallPenalty, cachingStrNoWallPenalty))
-		return false;
+		auto obstacles = battleGetAllObstaclesOnPos(hex, false);
 
-	const int wallInStackLine = lineToWallHex(shooterPosition.getY());
-	const bool shooterOutsideWalls = shooterPosition < wallInStackLine;
+		if(hex != ESiegeHex::GATE_BRIDGE || (battleIsGatePassable()))
+			for(const auto & obst : obstacles)
+				if(obst->obstacleType ==  CObstacleInstance::MOAT)
+					pathHasMoat |= true;
+	}
 
-	return shooterOutsideWalls && needWallPenalty(shooterPosition, destHex);
+	return checkNeeded && ( (checkWall && pathHasWall) || (checkMoat && pathHasMoat) );
 }
 
-si8 CBattleInfoCallback::battleCanTeleportTo(const battle::Unit * stack, BattleHex destHex, int telportLevel) const
+bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const
 {
 	RETURN_IF_NOT_BATTLE(false);
-	if (!getAccesibility(stack).accessible(destHex, stack))
+	if(!battleGetSiegeLevel())
 		return false;
 
-	const ui8 siegeLevel = battleGetSiegeLevel();
+	const std::string cachingStrNoWallPenalty = "type_NO_WALL_PENALTY";
+	static const auto selectorNoWallPenalty = Selector::type()(Bonus::NO_WALL_PENALTY);
+
+	if(shooter->hasBonus(selectorNoWallPenalty, cachingStrNoWallPenalty))
+		return false;
 
-	//check for wall
-	//advanced teleport can pass wall of fort|citadel, expert - of castle
-	if ((siegeLevel > CGTownInstance::NONE && telportLevel < 2) || (siegeLevel >= CGTownInstance::CASTLE && telportLevel < 3))
-		return sameSideOfWall(stack->getPosition(), destHex);
+	const auto shooterOutsideWalls = shooterPosition < lineToWallHex(shooterPosition.getY());
 
-	return true;
+	return shooterOutsideWalls && battleHasPenaltyOnLine(shooterPosition, destHex, true, false);
 }
 
 std::vector<PossiblePlayerBattleAction> CBattleInfoCallback::getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data)
@@ -271,7 +279,7 @@ PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * s
 	return PossiblePlayerBattleAction(spellSelMode, spell->id);
 }
 
-std::set<BattleHex> CBattleInfoCallback::battleGetAttackedHexes(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos) const
+std::set<BattleHex> CBattleInfoCallback::battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const
 {
 	std::set<BattleHex> attackedHexes;
 	RETURN_IF_NOT_BATTLE(attackedHexes);
@@ -645,7 +653,7 @@ std::vector<BattleHex> CBattleInfoCallback::battleGetAvailableHexes(const battle
 	return ret;
 }
 
-bool CBattleInfoCallback::battleCanAttack(const CStack * stack, const CStack * target, BattleHex dest) const
+bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const
 {
 	RETURN_IF_NOT_BATTLE(false);
 
@@ -658,7 +666,7 @@ bool CBattleInfoCallback::battleCanAttack(const CStack * stack, const CStack * t
 	if(!battleMatchOwner(stack, target))
 		return false;
 
-	auto id = stack->getCreature()->getId();
+	auto id = stack->unitType()->getId();
 	if (id == CreatureID::FIRST_AID_TENT || id == CreatureID::CATAPULT)
 		return false;
 
@@ -814,15 +822,74 @@ std::vector<std::shared_ptr<const CObstacleInstance>> CBattleInfoCallback::getAl
 						affectedObstacles.push_back(i);
 		}
 		for(auto hex : unit->getHexes())
-			if(hex == ESiegeHex::GATE_BRIDGE)
-				if(battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED)
-					for(int i=0; i<affectedObstacles.size(); i++)
-						if(affectedObstacles.at(i)->obstacleType == CObstacleInstance::MOAT)
-							affectedObstacles.erase(affectedObstacles.begin()+i);
+			if(hex == ESiegeHex::GATE_BRIDGE && battleIsGatePassable())
+				for(int i=0; i<affectedObstacles.size(); i++)
+					if(affectedObstacles.at(i)->obstacleType == CObstacleInstance::MOAT)
+						affectedObstacles.erase(affectedObstacles.begin()+i);
 	}
 	return affectedObstacles;
 }
 
+bool CBattleInfoCallback::handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set<BattleHex> & passed) const
+{
+	if(!unit.alive())
+		return false;
+	bool movementStopped = false;
+	for(auto & obstacle : getAllAffectedObstaclesByStack(&unit, passed))
+	{
+		//helper info
+		const SpellCreatedObstacle * spellObstacle = dynamic_cast<const SpellCreatedObstacle *>(obstacle.get());
+
+		if(spellObstacle)
+		{
+			auto revealObstacles = [&](const SpellCreatedObstacle & spellObstacle) -> void
+			{
+				// For the hidden spell created obstacles, e.g. QuickSand, it should be revealed after taking damage
+				auto operation = ObstacleChanges::EOperation::UPDATE;
+				if (spellObstacle.removeOnTrigger)
+					operation = ObstacleChanges::EOperation::REMOVE;
+
+				SpellCreatedObstacle changedObstacle;
+				changedObstacle.uniqueID = spellObstacle.uniqueID;
+				changedObstacle.revealed = true;
+
+				BattleObstaclesChanged bocp;
+				bocp.changes.emplace_back(spellObstacle.uniqueID, operation);
+				changedObstacle.toInfo(bocp.changes.back(), operation);
+				spellEnv.apply(&bocp);
+			};
+			const auto side = unit.unitSide();
+			auto shouldReveal = !spellObstacle->hidden || !battleIsObstacleVisibleForSide(*obstacle, (BattlePerspective::BattlePerspective)side);
+			const auto * hero = battleGetFightingHero(spellObstacle->casterSide);
+			auto caster = spells::ObstacleCasterProxy(getBattle()->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle);
+			const auto * sp = obstacle->getTrigger().toSpell();
+			if(obstacle->triggersEffects() && sp)
+			{
+				auto cast = spells::BattleCast(this, &caster, spells::Mode::PASSIVE, sp);
+				spells::detail::ProblemImpl ignored;
+				auto target = spells::Target(1, spells::Destination(&unit));
+				if(sp->battleMechanics(&cast)->canBeCastAt(target, ignored)) // Obstacles should not be revealed by immune creatures
+				{
+					if(shouldReveal) { //hidden obstacle triggers effects after revealed
+						revealObstacles(*spellObstacle);
+						cast.cast(&spellEnv, target);
+					}
+				}
+			}
+			else if(shouldReveal)
+				revealObstacles(*spellObstacle);
+		}
+
+		if(!unit.alive())
+			return false;
+
+		if(obstacle->stopsMovement())
+			movementStopped = true;
+	}
+
+	return unit.alive() && !movementStopped;
+}
+
 AccessibilityInfo CBattleInfoCallback::getAccesibility() const
 {
 	AccessibilityInfo ret;

+ 6 - 3
lib/battle/CBattleInfoCallback.h

@@ -20,6 +20,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class CGHeroInstance;
 class CStack;
 class ISpellCaster;
+class SpellCastEnvironment;
 class CSpell;
 struct CObstacleInstance;
 class IBonusBearer;
@@ -61,6 +62,8 @@ public:
 
 	std::vector<std::shared_ptr<const CObstacleInstance>> battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking = true) const override;
 	std::vector<std::shared_ptr<const CObstacleInstance>> getAllAffectedObstaclesByStack(const battle::Unit * unit, const std::set<BattleHex> & passed) const override;
+	//Handle obstacle damage here, requires SpellCastEnvironment
+	bool handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set<BattleHex> & passed = {}) const;
 
 	const CStack * battleGetStackByPos(BattleHex pos, bool onlyAlive = true) const;
 
@@ -83,10 +86,10 @@ public:
 
 	int battleGetSurrenderCost(const PlayerColor & Player) const; //returns cost of surrendering battle, -1 if surrendering is not possible
 	ReachabilityInfo::TDistances battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const;
-	std::set<BattleHex> battleGetAttackedHexes(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID) const;
+	std::set<BattleHex> battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID) const;
 	bool isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const;
 
-	bool battleCanAttack(const CStack * stack, const CStack * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination
+	bool battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination
 	bool battleCanShoot(const battle::Unit * attacker, BattleHex dest) const; //determines if stack with given ID shoot at the selected destination
 	bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle
 	bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack
@@ -101,6 +104,7 @@ public:
 	DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg = nullptr) const;
 	DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg = nullptr) const;
 
+	bool battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const;
 	bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const;
 	bool battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const;
 	bool battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const;
@@ -120,7 +124,6 @@ public:
 	SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const;
 	SpellID getRandomCastedSpell(CRandomGenerator & rand, const CStack * caster) const; //called at the beginning of turn for Faerie Dragon
 
-	si8 battleCanTeleportTo(const battle::Unit * stack, BattleHex destHex, int telportLevel) const; //checks if teleportation of given stack to given position can take place
 	std::vector<PossiblePlayerBattleAction> getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data);
 	PossiblePlayerBattleAction getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const;
 

+ 9 - 0
lib/battle/CBattleInfoEssentials.cpp

@@ -379,6 +379,15 @@ EGateState CBattleInfoEssentials::battleGetGateState() const
 	return getBattle()->getGateState();
 }
 
+bool CBattleInfoEssentials::battleIsGatePassable() const
+{
+	RETURN_IF_NOT_BATTLE(true);
+	if(battleGetSiegeLevel() == CGTownInstance::NONE)
+		return true;
+
+	return battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED; 
+}
+
 PlayerColor CBattleInfoEssentials::battleGetOwner(const battle::Unit * unit) const
 {
 	RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE);

+ 1 - 0
lib/battle/CBattleInfoEssentials.h

@@ -96,6 +96,7 @@ public:
 	// [3] - below gate, [4] - over gate, [5] - upper wall, [6] - uppert tower, [7] - gate; returned value: 1 - intact, 2 - damaged, 3 - destroyed; 0 - no battle
 	EWallState battleGetWallState(EWallPart partOfWall) const;
 	EGateState battleGetGateState() const;
+	bool battleIsGatePassable() const;
 
 	//helpers
 	///returns all stacks, alive or dead or undead or mechanical :)

+ 1 - 2
lib/battle/CCallbackBase.h

@@ -25,14 +25,13 @@ class DLL_LINKAGE CCallbackBase
 {
 	const IBattleInfo * battle = nullptr; //battle to which the player is engaged, nullptr if none or not applicable
 
-	const IBattleInfo * getBattle() const;
-
 protected:
 	boost::optional<PlayerColor> player; // not set gives access to all information, otherwise callback provides only information "visible" for player
 
 	CCallbackBase(boost::optional<PlayerColor> Player);
 	CCallbackBase() = default;
 
+	const IBattleInfo * getBattle() const;
 	void setBattle(const IBattleInfo * B);
 	bool duringBattle() const;
 

+ 29 - 25
lib/spells/effects/Teleport.cpp

@@ -14,6 +14,7 @@
 #include "../ISpellMechanics.h"
 #include "../../NetPacks.h"
 #include "../../battle/CBattleInfoCallback.h"
+#include "../../serializer/JsonSerializeFormat.h"
 #include "../../battle/Unit.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -45,39 +46,34 @@ void Teleport::adjustTargetTypes(std::vector<TargetType> & types) const
 	}
 }
 
-bool Teleport::applicable(Problem & problem, const Mechanics * m) const
+bool Teleport::applicable(Problem & problem, const Mechanics * m, const EffectTarget & target) const
 {
-	return UnitEffect::applicable(problem, m);
-}
+	if(target.size() == 1) //Assume, this is check only for selecting a unit
+		return UnitEffect::applicable(problem, m);
 
-void Teleport::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const
-{
 	if(target.size() != 2)
-	{
-		server->complain("Teleport requires 2 destinations.");
-		return;
-	}
+		return m->adaptProblem(ESpellCastProblem::WRONG_SPELL_TARGET, problem);
 
 	const auto *targetUnit = target[0].unitValue;
-	if(nullptr == targetUnit)
-	{
-		server->complain("No unit to teleport");
-		return;
-	}
+	const auto & targetHex = target[1].hexValue;
 
-	const BattleHex destination = target[1].hexValue;
-	if(!destination.isValid())
-	{
-		server->complain("Invalid teleport destination");
-		return;
-	}
+	if(!targetUnit)
+		return m->adaptProblem(ESpellCastProblem::WRONG_SPELL_TARGET, problem);
 
-	//TODO: move here all teleport checks
-	if(!m->battle()->battleCanTeleportTo(targetUnit, destination, m->getEffectLevel()))
+	if(!targetHex.isValid() || !m->battle()->getAccesibility(targetUnit).accessible(targetHex, targetUnit))
+		return m->adaptProblem(ESpellCastProblem::WRONG_SPELL_TARGET, problem);
+
+	if(m->battle()->battleGetSiegeLevel() && !(isWallPassable && isMoatPassable))
 	{
-		server->complain("Forbidden teleport.");
-		return;
+		return !m->battle()->battleHasPenaltyOnLine(target[0].hexValue, target[1].hexValue, !isWallPassable, !isMoatPassable);
 	}
+	return true;
+}
+
+void Teleport::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const
+{
+	const auto *targetUnit = target[0].unitValue;
+	const auto destination = target[1].hexValue;
 
 	BattleStackMoved pack;
 	pack.distance = 0;
@@ -87,11 +83,19 @@ void Teleport::apply(ServerCallback * server, const Mechanics * m, const EffectT
 	pack.tilesToMove = tiles;
 	pack.teleporting = true;
 	server->apply(&pack);
+
+	if(triggerObstacles)
+	{
+		auto spellEnv = dynamic_cast<SpellCastEnvironment*>(server);
+		m->battle()->handleObstacleTriggersForUnit(*spellEnv, *targetUnit);
+	}
 }
 
 void Teleport::serializeJsonUnitEffect(JsonSerializeFormat & handler)
 {
-	//TODO: teleport options
+	handler.serializeBool("triggerObstacles", triggerObstacles);
+	handler.serializeBool("isWallPassable", isWallPassable);
+	handler.serializeBool("isMoatPassable", isMoatPassable);
 }
 
 EffectTarget Teleport::transformTarget(const Mechanics * m, const Target & aimPoint, const Target & spellTarget) const

+ 6 - 1
lib/spells/effects/Teleport.h

@@ -26,7 +26,7 @@ class Teleport : public UnitEffect
 public:
 	void adjustTargetTypes(std::vector<TargetType> & types) const override;
 
-	bool applicable(Problem & problem, const Mechanics * m) const override;
+	bool applicable(Problem & problem, const Mechanics * m, const EffectTarget & target) const override;
 
 	void apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const override;
 
@@ -34,6 +34,11 @@ public:
 
 protected:
 	void serializeJsonUnitEffect(JsonSerializeFormat & handler) override;
+
+private:
+	bool triggerObstacles;
+	bool isWallPassable;
+	bool isMoatPassable;
 };
 
 }

+ 16 - 76
server/CGameHandler.cpp

@@ -1208,11 +1208,11 @@ void CGameHandler::addGenericKilledLog(BattleLogMessage & blm, const CStack * de
 		boost::format txt(formatString);
 		if(killed > 1)
 		{
-			txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->getCreature()->getNamePluralTranslated()); // creatures perish
+			txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish
 		}
 		else //killed == 1
 		{
-			txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->getCreature()->getNameSingularTranslated()); // creature perishes
+			txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes
 		}
 		MetaString line;
 		line << txt.str();
@@ -1527,7 +1527,7 @@ int CGameHandler::moveStack(int stack, BattleHex dest)
 			{
 				if(stackIsMoving && start != curStack->getPosition())
 				{
-					stackIsMoving = handleDamageFromObstacle(curStack, passed);
+					stackIsMoving = handleObstacleTriggersForUnit(*spellEnv, *curStack, passed);
 					passed.insert(curStack->getPosition());
 					if(curStack->doubleWide())
 						passed.insert(curStack->occupiedHex());
@@ -1565,7 +1565,7 @@ int CGameHandler::moveStack(int stack, BattleHex dest)
 			passed.clear(); //Just empty passed, obstacles will handled automatically
 	}
 	//handling obstacle on the final field (separate, because it affects both flying and walking stacks)
-	handleDamageFromObstacle(curStack, passed);
+	handleObstacleTriggersForUnit(*spellEnv, *curStack, passed);
 
 	return ret;
 }
@@ -4478,10 +4478,10 @@ void CGameHandler::updateGateState()
 	// - deals moat damage to attacker if bridge is closed (fortress only)
 
 	bool hasForceFieldOnBridge = !battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), true).empty();
-	bool hasStackAtGateInner   = gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr;
-	bool hasStackAtGateOuter   = gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr;
-	bool hasStackAtGateBridge  = gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false) != nullptr;
-	bool hasWideMoat         = vstd::contains_if(battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr<const CObstacleInstance> & obst)
+	bool hasStackAtGateInner   = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr;
+	bool hasStackAtGateOuter   = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr;
+	bool hasStackAtGateBridge  = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false) != nullptr;
+	bool hasWideMoat           = vstd::contains_if(battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr<const CObstacleInstance> & obst)
 	{
 		return obst->obstacleType == CObstacleInstance::MOAT;
 	});
@@ -4933,7 +4933,7 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 	}
 	if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND
 			|| ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL)
-		handleDamageFromObstacle(stack);
+		handleObstacleTriggersForUnit(*spellEnv, *stack);
 	if(ba.stackNumber == gs->curB->activeStack || battleResult.get()) //active stack has moved or battle has finished
 		battleMadeAction.setn(true);
 	return ok;
@@ -5289,66 +5289,6 @@ void CGameHandler::stackTurnTrigger(const CStack *st)
 	}
 }
 
-bool CGameHandler::handleDamageFromObstacle(const battle::Unit * curStack, const std::set<BattleHex> & passed)
-{
-	if(!curStack->alive())
-		return false;
-	bool movementStopped = false;
-	for(auto & obstacle : getAllAffectedObstaclesByStack(curStack, passed))
-	{
-		//helper info
-		const SpellCreatedObstacle * spellObstacle = dynamic_cast<const SpellCreatedObstacle *>(obstacle.get());
-
-		if(spellObstacle)
-		{
-			auto revealObstacles = [&](const SpellCreatedObstacle & spellObstacle) -> void
-			{
-				// For the hidden spell created obstacles, e.g. QuickSand, it should be revealed after taking damage
-				auto operation = ObstacleChanges::EOperation::UPDATE;
-				if (spellObstacle.removeOnTrigger)
-					operation = ObstacleChanges::EOperation::REMOVE;
-
-				SpellCreatedObstacle changedObstacle;
-				changedObstacle.uniqueID = spellObstacle.uniqueID;
-				changedObstacle.revealed = true;
-
-				BattleObstaclesChanged bocp;
-				bocp.changes.emplace_back(spellObstacle.uniqueID, operation);
-				changedObstacle.toInfo(bocp.changes.back(), operation);
-				sendAndApply(&bocp);
-			};
-			const auto side = curStack->unitSide();
-			auto shouldReveal = !spellObstacle->hidden || !gs->curB->battleIsObstacleVisibleForSide(*obstacle, (BattlePerspective::BattlePerspective)side);
-			const auto * hero = gs->curB->battleGetFightingHero(spellObstacle->casterSide);
-			auto caster = spells::ObstacleCasterProxy(gs->curB->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle);
-			const auto * sp = obstacle->getTrigger().toSpell();
-			if(obstacle->triggersEffects() && sp)
-			{
-				auto cast = spells::BattleCast(gs->curB, &caster, spells::Mode::PASSIVE, sp);
-				spells::detail::ProblemImpl ignored;
-				auto target = spells::Target(1, spells::Destination(curStack));
-				if(sp->battleMechanics(&cast)->canBeCastAt(target, ignored)) // Obstacles should not be revealed by immune creatures
-				{
-					if(shouldReveal) { //hidden obstacle triggers effects after revealed
-						revealObstacles(*spellObstacle);
-						cast.cast(spellEnv, target);
-					}
-				}
-			}
-			else if(shouldReveal)
-				revealObstacles(*spellObstacle);
-		}
-
-		if(!curStack->alive())
-			return false;
-
-		if(obstacle->stopsMovement())
-			movementStopped = true;
-	}
-
-	return curStack->alive() && !movementStopped;
-}
-
 void CGameHandler::handleTimeEvents()
 {
 	gs->map->events.sort(evntCmp);
@@ -6031,8 +5971,8 @@ void CGameHandler::handleAfterAttackCasting(bool ranged, const CStack * attacker
 
 		int bonusAdditionalInfo = attacker->getBonus(Selector::type()(Bonus::TRANSMUTATION))->additionalInfo[0];
 
-		if(defender->getCreature()->getId() == bonusAdditionalInfo ||
-			(bonusAdditionalInfo == CAddInfo::NONE && defender->getCreature()->getId() == attacker->getCreature()->getId()))
+		if(defender->unitType()->getId() == bonusAdditionalInfo ||
+			(bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId()))
 			return;
 
 		battle::UnitInfo resurrectInfo;
@@ -6421,7 +6361,7 @@ void CGameHandler::runBattle()
 			auto accessibility = getAccesibility();
 			CreatureID creatureData = CreatureID(summonInfo->subtype);
 			std::vector<BattleHex> targetHexes;
-			const bool targetIsBig = stack->getCreature()->isDoubleWide(); //target = creature to guard
+			const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard
 			const bool guardianIsBig = creatureData.toCreature()->isDoubleWide();
 
 			/*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible.
@@ -6606,7 +6546,7 @@ void CGameHandler::runBattle()
 			}
 
 			const CGHeroInstance * curOwner = battleGetOwnerHero(next);
-			const int stackCreatureId = next->getCreature()->getId();
+			const int stackCreatureId = next->unitType()->getId();
 
 			if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA)
 				&& (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(Bonus::MANUAL_CONTROL, stackCreatureId)))
@@ -6622,7 +6562,7 @@ void CGameHandler::runBattle()
 
 				for(auto & elem : gs->curB->stacks)
 				{
-					if(elem->getCreature()->getId() != CreatureID::CATAPULT
+					if(elem->unitType()->getId() != CreatureID::CATAPULT
 						&& elem->owner != next->owner
 						&& elem->isValidTarget()
 						&& gs->curB->battleCanShoot(next, elem->getPosition()))
@@ -6644,7 +6584,7 @@ void CGameHandler::runBattle()
 				continue;
 			}
 
-			if (next->getCreature()->getId() == CreatureID::CATAPULT)
+			if (next->unitType()->getId() == CreatureID::CATAPULT)
 			{
 				const auto & attackableBattleHexes = curB.getAttackableBattleHexes();
 
@@ -6666,7 +6606,7 @@ void CGameHandler::runBattle()
 				}
 			}
 
-			if (next->getCreature()->getId() == CreatureID::FIRST_AID_TENT)
+			if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT)
 			{
 				TStacks possibleStacks = battleGetStacksIf([=](const CStack * s)
 				{

+ 0 - 1
server/CGameHandler.h

@@ -236,7 +236,6 @@ public:
 	bool makeCustomAction(BattleAction &ba);
 	void stackEnchantedTrigger(const CStack * stack);
 	void stackTurnTrigger(const CStack *stack);
-	bool handleDamageFromObstacle(const battle::Unit * curStack, const std::set<BattleHex> & passed = {}); //checks if obstacle is land mine and handles possible consequences
 
 	void removeObstacle(const CObstacleInstance &obstacle);
 	bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player );