mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	Split mechanics implementation
This commit is contained in:
		| @@ -150,8 +150,6 @@ | ||||
| 		<Unit filename="CRandomGenerator.h" /> | ||||
| 		<Unit filename="CScriptingModule.h" /> | ||||
| 		<Unit filename="CSoundBase.h" /> | ||||
| 		<Unit filename="CSpellHandler.cpp" /> | ||||
| 		<Unit filename="CSpellHandler.h" /> | ||||
| 		<Unit filename="CStopWatch.h" /> | ||||
| 		<Unit filename="CThreadHelper.cpp" /> | ||||
| 		<Unit filename="CThreadHelper.h" /> | ||||
| @@ -185,8 +183,6 @@ | ||||
| 		<Unit filename="ResourceSet.cpp" /> | ||||
| 		<Unit filename="ResourceSet.h" /> | ||||
| 		<Unit filename="ScopeGuard.h" /> | ||||
| 		<Unit filename="SpellMechanics.cpp" /> | ||||
| 		<Unit filename="SpellMechanics.h" /> | ||||
| 		<Unit filename="StartInfo.h" /> | ||||
| 		<Unit filename="StdInc.h"> | ||||
| 			<Option weight="0" /> | ||||
| @@ -294,6 +290,18 @@ | ||||
| 		<Unit filename="rmg/CZoneGraphGenerator.h" /> | ||||
| 		<Unit filename="rmg/CZonePlacer.cpp" /> | ||||
| 		<Unit filename="rmg/CZonePlacer.h" /> | ||||
| 		<Unit filename="spells/AdventureSpellMechanics.cpp" /> | ||||
| 		<Unit filename="spells/AdventureSpellMechanics.h" /> | ||||
| 		<Unit filename="spells/BattleSpellMechanics.cpp" /> | ||||
| 		<Unit filename="spells/BattleSpellMechanics.h" /> | ||||
| 		<Unit filename="spells/CDefaultSpellMechanics.cpp" /> | ||||
| 		<Unit filename="spells/CDefaultSpellMechanics.h" /> | ||||
| 		<Unit filename="spells/CSpellHandler.cpp" /> | ||||
| 		<Unit filename="spells/CSpellHandler.h" /> | ||||
| 		<Unit filename="spells/CreatureSpellMechanics.cpp" /> | ||||
| 		<Unit filename="spells/CreatureSpellMechanics.h" /> | ||||
| 		<Unit filename="spells/ISpellMechanics.cpp" /> | ||||
| 		<Unit filename="spells/ISpellMechanics.h" /> | ||||
| 		<Unit filename="vcmi_endian.h" /> | ||||
| 		<Extensions> | ||||
| 			<code_completion /> | ||||
|   | ||||
| @@ -11,3 +11,245 @@ | ||||
| #include "StdInc.h" | ||||
|  | ||||
| #include "AdventureSpellMechanics.h" | ||||
|  | ||||
| #include "../CRandomGenerator.h" | ||||
| #include "../mapObjects/CGHeroInstance.h" | ||||
| #include "../NetPacks.h" | ||||
| #include "../BattleState.h" | ||||
| #include "../CGameState.h" | ||||
| #include "../CGameInfoCallback.h" | ||||
|  | ||||
|  | ||||
| ///AdventureBonusingMechanics | ||||
| bool AdventureBonusingMechanics::applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const | ||||
| { | ||||
| 	const int schoolLevel = parameters.caster->getSpellSchoolLevel(owner);	 | ||||
| 	const int subtype = schoolLevel >= 2 ? 1 : 2; //adv or expert | ||||
|  | ||||
| 	GiveBonus gb; | ||||
| 	gb.id = parameters.caster->id.getNum(); | ||||
| 	gb.bonus = Bonus(Bonus::ONE_DAY, bonusTypeID, Bonus::SPELL_EFFECT, 0, owner->id, subtype); | ||||
| 	env->sendAndApply(&gb);	 | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| ///SummonBoatMechanics | ||||
| bool SummonBoatMechanics::applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const | ||||
| { | ||||
| 	const int schoolLevel = parameters.caster->getSpellSchoolLevel(owner);	 | ||||
| 	//check if spell works at all | ||||
| 	if(env->getRandomGenerator().nextInt(99) >= owner->getPower(schoolLevel)) //power is % chance of success | ||||
| 	{ | ||||
| 		InfoWindow iw; | ||||
| 		iw.player = parameters.caster->tempOwner; | ||||
| 		iw.text.addTxt(MetaString::GENERAL_TXT, 336); //%s tried to summon a boat, but failed. | ||||
| 		iw.text.addReplacement(parameters.caster->name); | ||||
| 		env->sendAndApply(&iw); | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	//try to find unoccupied boat to summon | ||||
| 	const CGBoat * nearest = nullptr; | ||||
| 	double dist = 0; | ||||
| 	int3 summonPos = parameters.caster->bestLocation(); | ||||
| 	if(summonPos.x < 0) | ||||
| 	{ | ||||
| 		env->complain("There is no water tile available!"); | ||||
| 		return false; | ||||
| 	}	 | ||||
| 	 | ||||
| 	for(const CGObjectInstance * obj : env->getMap()->objects) | ||||
| 	{ | ||||
| 		if(obj && obj->ID == Obj::BOAT) | ||||
| 		{ | ||||
| 			const CGBoat *b = static_cast<const CGBoat*>(obj); | ||||
| 			if(b->hero)  | ||||
| 				continue; //we're looking for unoccupied boat | ||||
|  | ||||
| 			double nDist = b->pos.dist2d(parameters.caster->getPosition()); | ||||
| 			if(!nearest || nDist < dist) //it's first boat or closer than previous | ||||
| 			{ | ||||
| 				nearest = b; | ||||
| 				dist = nDist; | ||||
| 			} | ||||
| 		}						 | ||||
| 	} | ||||
|  | ||||
| 	if(nullptr != nearest) //we found boat to summon | ||||
| 	{ | ||||
| 		ChangeObjPos cop; | ||||
| 		cop.objid = nearest->id; | ||||
| 		cop.nPos = summonPos + int3(1,0,0);; | ||||
| 		cop.flags = 1; | ||||
| 		env->sendAndApply(&cop); | ||||
| 	} | ||||
| 	else if(schoolLevel < 2) //none or basic level -> cannot create boat :( | ||||
| 	{ | ||||
| 		InfoWindow iw; | ||||
| 		iw.player = parameters.caster->tempOwner; | ||||
| 		iw.text.addTxt(MetaString::GENERAL_TXT, 335); //There are no boats to summon. | ||||
| 		env->sendAndApply(&iw); | ||||
| 	} | ||||
| 	else //create boat | ||||
| 	{ | ||||
| 		NewObject no; | ||||
| 		no.ID = Obj::BOAT; | ||||
| 		no.subID = parameters.caster->getBoatType(); | ||||
| 		no.pos = summonPos + int3(1,0,0);; | ||||
| 		env->sendAndApply(&no); | ||||
| 	}	 | ||||
| 	return true;	 | ||||
| } | ||||
|  | ||||
| ///ScuttleBoatMechanics | ||||
| bool ScuttleBoatMechanics::applyAdventureEffects(const SpellCastEnvironment* env, AdventureSpellCastParameters& parameters) const | ||||
| { | ||||
| 	const int schoolLevel = parameters.caster->getSpellSchoolLevel(owner); | ||||
| 	//check if spell works at all | ||||
| 	if(env->getRandomGenerator().nextInt(99) >= owner->getPower(schoolLevel)) //power is % chance of success | ||||
| 	{ | ||||
| 		InfoWindow iw; | ||||
| 		iw.player = parameters.caster->tempOwner; | ||||
| 		iw.text.addTxt(MetaString::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed | ||||
| 		iw.text.addReplacement(parameters.caster->name); | ||||
| 		env->sendAndApply(&iw); | ||||
| 		return true; | ||||
| 	} | ||||
| 		 | ||||
| 	if(!env->getMap()->isInTheMap(parameters.pos)) | ||||
| 	{ | ||||
| 		env->complain("Invalid dst tile for scuttle!"); | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	//TODO: test range, visibility | ||||
| 	const TerrainTile *t = &env->getMap()->getTile(parameters.pos); | ||||
| 	if(!t->visitableObjects.size() || t->visitableObjects.back()->ID != Obj::BOAT) | ||||
| 	{ | ||||
| 		env->complain("There is no boat to scuttle!"); | ||||
| 		return false; | ||||
| 	} | ||||
| 		 | ||||
|  | ||||
| 	RemoveObject ro; | ||||
| 	ro.id = t->visitableObjects.back()->id; | ||||
| 	env->sendAndApply(&ro); | ||||
| 	return true;	 | ||||
| } | ||||
|  | ||||
| ///DimensionDoorMechanics | ||||
| bool DimensionDoorMechanics::applyAdventureEffects(const SpellCastEnvironment* env, AdventureSpellCastParameters& parameters) const | ||||
| { | ||||
| 	if(!env->getMap()->isInTheMap(parameters.pos)) | ||||
| 	{ | ||||
| 		env->complain("Destination is out of map!"); | ||||
| 		return false; | ||||
| 	}	 | ||||
| 	 | ||||
| 	const TerrainTile * dest = env->getCb()->getTile(parameters.pos); | ||||
| 	const TerrainTile * curr = env->getCb()->getTile(parameters.caster->getSightCenter()); | ||||
|  | ||||
| 	if(nullptr == dest) | ||||
| 	{ | ||||
| 		env->complain("Destination tile doesn't exist!"); | ||||
| 		return false; | ||||
| 	} | ||||
| 	 | ||||
| 	if(nullptr == curr) | ||||
| 	{ | ||||
| 		env->complain("Source tile doesn't exist!"); | ||||
| 		return false; | ||||
| 	}	 | ||||
| 		 | ||||
| 	if(parameters.caster->movement <= 0) | ||||
| 	{ | ||||
| 		env->complain("Hero needs movement points to cast Dimension Door!"); | ||||
| 		return false; | ||||
| 	} | ||||
| 	 | ||||
| 	const int schoolLevel = parameters.caster->getSpellSchoolLevel(owner); | ||||
| 			 | ||||
| 	if(parameters.caster->getBonusesCount(Bonus::SPELL_EFFECT, SpellID::DIMENSION_DOOR) >= owner->getPower(schoolLevel)) //limit casts per turn | ||||
| 	{ | ||||
| 		InfoWindow iw; | ||||
| 		iw.player = parameters.caster->tempOwner; | ||||
| 		iw.text.addTxt(MetaString::GENERAL_TXT, 338); //%s is not skilled enough to cast this spell again today. | ||||
| 		iw.text.addReplacement(parameters.caster->name); | ||||
| 		env->sendAndApply(&iw); | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	GiveBonus gb; | ||||
| 	gb.id = parameters.caster->id.getNum(); | ||||
| 	gb.bonus = Bonus(Bonus::ONE_DAY, Bonus::NONE, Bonus::SPELL_EFFECT, 0, owner->id); | ||||
| 	env->sendAndApply(&gb); | ||||
|  | ||||
| 	if(!dest->isClear(curr)) //wrong dest tile | ||||
| 	{ | ||||
| 		InfoWindow iw; | ||||
| 		iw.player = parameters.caster->tempOwner; | ||||
| 		iw.text.addTxt(MetaString::GENERAL_TXT, 70); //Dimension Door failed! | ||||
| 		env->sendAndApply(&iw);		 | ||||
| 	} | ||||
| 	else if(env->moveHero(parameters.caster->id, parameters.pos + parameters.caster->getVisitableOffset(), true)) | ||||
| 	{ | ||||
| 		SetMovePoints smp; | ||||
| 		smp.hid = parameters.caster->id; | ||||
| 		smp.val = std::max<ui32>(0, parameters.caster->movement - 300); | ||||
| 		env->sendAndApply(&smp); | ||||
| 	} | ||||
| 	return true; 	 | ||||
| } | ||||
|  | ||||
| ///TownPortalMechanics | ||||
| bool TownPortalMechanics::applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters& parameters) const | ||||
| { | ||||
| 	if (!env->getMap()->isInTheMap(parameters.pos)) | ||||
| 	{ | ||||
| 		env->complain("Destination tile not present!"); | ||||
| 		return false; | ||||
| 	}	 | ||||
|  | ||||
| 	TerrainTile tile = env->getMap()->getTile(parameters.pos); | ||||
| 	if (tile.visitableObjects.empty() || tile.visitableObjects.back()->ID != Obj::TOWN) | ||||
| 	{ | ||||
| 		env->complain("Town not found for Town Portal!");	 | ||||
| 		return false; | ||||
| 	}		 | ||||
|  | ||||
| 	CGTownInstance * town = static_cast<CGTownInstance*>(tile.visitableObjects.back()); | ||||
| 	if (town->tempOwner != parameters.caster->tempOwner) | ||||
| 	{ | ||||
| 		env->complain("Can't teleport to another player!"); | ||||
| 		return false; | ||||
| 	}			 | ||||
| 	 | ||||
| 	if (town->visitingHero) | ||||
| 	{ | ||||
| 		env->complain("Can't teleport to occupied town!"); | ||||
| 		return false; | ||||
| 	} | ||||
| 	 | ||||
| 	if (parameters.caster->getSpellSchoolLevel(owner) < 2) | ||||
| 	{ | ||||
| 		si32 dist = town->pos.dist2dSQ(parameters.caster->pos); | ||||
| 		ObjectInstanceID nearest = town->id; //nearest town's ID | ||||
| 		for(const CGTownInstance * currTown : env->getCb()->getPlayer(parameters.caster->tempOwner)->towns) | ||||
| 		{ | ||||
| 			si32 currDist = currTown->pos.dist2dSQ(parameters.caster->pos); | ||||
| 			if (currDist < dist) | ||||
| 			{ | ||||
| 				nearest = currTown->id; | ||||
| 				dist = currDist; | ||||
| 			} | ||||
| 		} | ||||
| 		if (town->id != nearest) | ||||
| 		{ | ||||
| 			env->complain("This hero can only teleport to nearest town!"); | ||||
| 			return false; | ||||
| 		} | ||||
| 			 | ||||
| 	} | ||||
| 	env->moveHero(parameters.caster->id, town->visitablePos() + parameters.caster->getVisitableOffset() ,1);	 | ||||
| 	return true; | ||||
| } | ||||
|   | ||||
| @@ -11,3 +11,47 @@ | ||||
|  #pragma once | ||||
|   | ||||
|  #include "CDefaultSpellMechanics.h" | ||||
|  | ||||
|  | ||||
| //todo: make configurable | ||||
| class AdventureBonusingMechanics: public DefaultSpellMechanics  | ||||
| { | ||||
| public:	 | ||||
| 	AdventureBonusingMechanics(CSpell * s, Bonus::BonusType _bonusTypeID): DefaultSpellMechanics(s), bonusTypeID(_bonusTypeID){};	 | ||||
| protected: | ||||
| 	bool applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override;	 | ||||
| private: | ||||
| 	Bonus::BonusType bonusTypeID; | ||||
| }; | ||||
|  | ||||
| class SummonBoatMechanics: public DefaultSpellMechanics  | ||||
| { | ||||
| public: | ||||
| 	SummonBoatMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| protected: | ||||
| 	bool applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override;	 | ||||
| }; | ||||
|  | ||||
| class ScuttleBoatMechanics: public DefaultSpellMechanics  | ||||
| { | ||||
| public: | ||||
| 	ScuttleBoatMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| protected: | ||||
| 	bool applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override;	 | ||||
| }; | ||||
|  | ||||
| class DimensionDoorMechanics: public DefaultSpellMechanics  | ||||
| { | ||||
| public:	 | ||||
| 	DimensionDoorMechanics(CSpell * s): DefaultSpellMechanics(s){};	 | ||||
| protected: | ||||
| 	bool applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override;	 | ||||
| }; | ||||
|  | ||||
| class TownPortalMechanics: public DefaultSpellMechanics  | ||||
| { | ||||
| public:	 | ||||
| 	TownPortalMechanics(CSpell * s): DefaultSpellMechanics(s){};	 | ||||
| protected: | ||||
| 	bool applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override;	 | ||||
| }; | ||||
|   | ||||
| @@ -11,3 +11,409 @@ | ||||
| #include "StdInc.h" | ||||
|  | ||||
| #include "BattleSpellMechanics.h" | ||||
|  | ||||
| #include "../NetPacks.h" | ||||
| #include "../BattleState.h" | ||||
|  | ||||
| ///ChainLightningMechanics | ||||
| std::set<const CStack *> ChainLightningMechanics::getAffectedStacks(SpellTargetingContext & ctx) const | ||||
| { | ||||
| 	std::set<const CStack* > attackedCres; | ||||
| 	 | ||||
| 	std::set<BattleHex> possibleHexes; | ||||
| 	for(auto stack : ctx.cb->battleGetAllStacks()) | ||||
| 	{ | ||||
| 		if(stack->isValidTarget()) | ||||
| 		{ | ||||
| 			for(auto hex : stack->getHexes()) | ||||
| 			{ | ||||
| 				possibleHexes.insert (hex); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	int targetsOnLevel[4] = {4, 4, 5, 5}; | ||||
|  | ||||
| 	BattleHex lightningHex = ctx.destination; | ||||
| 	for(int i = 0; i < targetsOnLevel[ctx.schoolLvl]; ++i) | ||||
| 	{ | ||||
| 		auto stack = ctx.cb->battleGetStackByPos(lightningHex, true); | ||||
| 		if(!stack) | ||||
| 			break; | ||||
| 		attackedCres.insert (stack); | ||||
| 		for(auto hex : stack->getHexes()) | ||||
| 		{ | ||||
| 			possibleHexes.erase(hex); //can't hit same place twice | ||||
| 		} | ||||
| 		if(possibleHexes.empty()) //not enough targets | ||||
| 			break; | ||||
| 		lightningHex = BattleHex::getClosestTile(stack->attackerOwned, ctx.destination, possibleHexes); | ||||
| 	}	 | ||||
| 		 | ||||
| 	return attackedCres; | ||||
| } | ||||
|  | ||||
| ///CloneMechanics | ||||
| void CloneMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const | ||||
| { | ||||
| 	const CStack * clonedStack = nullptr; | ||||
| 	if(ctx.attackedCres.size()) | ||||
| 		clonedStack = *ctx.attackedCres.begin(); | ||||
| 	if(!clonedStack) | ||||
| 	{ | ||||
| 		env->complain ("No target stack to clone!"); | ||||
| 		return; | ||||
| 	} | ||||
| 	const int attacker = !(bool)parameters.casterSide;  | ||||
|  | ||||
| 	BattleStackAdded bsa; | ||||
| 	bsa.creID = clonedStack->type->idNumber; | ||||
| 	bsa.attacker = attacker; | ||||
| 	bsa.summoned = true; | ||||
| 	bsa.pos = parameters.cb->getAvaliableHex(bsa.creID, attacker); //TODO: unify it | ||||
| 	bsa.amount = clonedStack->count; | ||||
| 	env->sendAndApply(&bsa); | ||||
|  | ||||
| 	BattleSetStackProperty ssp; | ||||
| 	ssp.stackID = bsa.newStackID;//we know stack ID after apply | ||||
| 	ssp.which = BattleSetStackProperty::CLONED; | ||||
| 	ssp.val = 0; | ||||
| 	ssp.absolute = 1; | ||||
| 	env->sendAndApply(&ssp);	 | ||||
| } | ||||
|  | ||||
| ESpellCastProblem::ESpellCastProblem CloneMechanics::isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const | ||||
| { | ||||
| 	//can't clone already cloned creature | ||||
| 	if(vstd::contains(obj->state, EBattleStackState::CLONED)) | ||||
| 		return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; | ||||
| 	//TODO: how about stacks casting Clone? | ||||
| 	//currently Clone casted by stack is assumed Expert level | ||||
| 	ui8 schoolLevel; | ||||
| 	if(caster) | ||||
| 	{ | ||||
| 		schoolLevel = caster->getSpellSchoolLevel(owner); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		schoolLevel = 3; | ||||
| 	} | ||||
|  | ||||
| 	if(schoolLevel < 3) | ||||
| 	{ | ||||
| 		int maxLevel = (std::max(schoolLevel, (ui8)1) + 4); | ||||
| 		int creLevel = obj->getCreature()->level; | ||||
| 		if(maxLevel < creLevel) //tier 1-5 for basic, 1-6 for advanced, any level for expert | ||||
| 			return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; | ||||
| 	} | ||||
| 	//use default algorithm only if there is no mechanics-related problem		 | ||||
| 	return DefaultSpellMechanics::isImmuneByStack(caster, obj);	 | ||||
| } | ||||
|  | ||||
| ///CureMechanics | ||||
| void CureMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const | ||||
| { | ||||
| 	DefaultSpellMechanics::applyBattle(battle, packet); | ||||
| 	 | ||||
| 	for(auto stackID : packet->affectedCres) | ||||
| 	{ | ||||
| 		if(vstd::contains(packet->resisted, stackID)) | ||||
| 		{ | ||||
| 			logGlobal->errorStream() << "Resistance to positive spell CURE"; | ||||
| 			continue; | ||||
| 		}			 | ||||
|  | ||||
| 		CStack *s = battle->getStack(stackID); | ||||
| 		s->popBonuses([&](const Bonus *b) -> bool | ||||
| 		{ | ||||
| 			if(b->source == Bonus::SPELL_EFFECT) | ||||
| 			{ | ||||
| 				CSpell * sp = SpellID(b->sid).toSpell(); | ||||
| 				return sp->isNegative(); | ||||
| 			} | ||||
| 			return false; //not a spell effect | ||||
| 		}); | ||||
| 	}		 | ||||
| } | ||||
|  | ||||
| ///DispellMechanics | ||||
| void DispellMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const | ||||
| { | ||||
| 	DefaultSpellMechanics::applyBattle(battle, packet); | ||||
| 	 | ||||
| 	for(auto stackID : packet->affectedCres) | ||||
| 	{ | ||||
| 		if(vstd::contains(packet->resisted, stackID)) | ||||
| 			continue; | ||||
|  | ||||
| 		CStack *s = battle->getStack(stackID); | ||||
| 		s->popBonuses([&](const Bonus *b) -> bool | ||||
| 		{ | ||||
| 			return Selector::sourceType(Bonus::SPELL_EFFECT)(b); | ||||
| 		}); | ||||
| 	}	 | ||||
| } | ||||
|  | ||||
|  | ||||
| ///HypnotizeMechanics | ||||
| ESpellCastProblem::ESpellCastProblem HypnotizeMechanics::isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const | ||||
| { | ||||
| 	if(nullptr != caster) //do not resist hypnotize casted after attack, for example | ||||
| 	{ | ||||
| 		//TODO: what with other creatures casting hypnotize, Faerie Dragons style? | ||||
| 		ui64 subjectHealth = (obj->count - 1) * obj->MaxHealth() + obj->firstHPleft; | ||||
| 		//apply 'damage' bonus for hypnotize, including hero specialty | ||||
| 		ui64 maxHealth = owner->calculateBonus(caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER) | ||||
| 			* owner->power + owner->getPower(caster->getSpellSchoolLevel(owner)), caster, obj); | ||||
| 		if (subjectHealth > maxHealth) | ||||
| 			return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; | ||||
| 	}			 | ||||
| 	return DefaultSpellMechanics::isImmuneByStack(caster, obj); | ||||
| } | ||||
|  | ||||
| ///ObstacleMechanics | ||||
| void ObstacleMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const | ||||
| { | ||||
| 	auto placeObstacle = [&, this](BattleHex pos) | ||||
| 	{ | ||||
| 		static int obstacleIdToGive =  parameters.cb->obstacles.size() | ||||
| 									? (parameters.cb->obstacles.back()->uniqueID+1) | ||||
| 									: 0; | ||||
|  | ||||
| 		auto obstacle = make_shared<SpellCreatedObstacle>(); | ||||
| 		switch(owner->id) // :/ | ||||
| 		{ | ||||
| 		case SpellID::QUICKSAND: | ||||
| 			obstacle->obstacleType = CObstacleInstance::QUICKSAND; | ||||
| 			obstacle->turnsRemaining = -1; | ||||
| 			obstacle->visibleForAnotherSide = false; | ||||
| 			break; | ||||
| 		case SpellID::LAND_MINE: | ||||
| 			obstacle->obstacleType = CObstacleInstance::LAND_MINE; | ||||
| 			obstacle->turnsRemaining = -1; | ||||
| 			obstacle->visibleForAnotherSide = false; | ||||
| 			break; | ||||
| 		case SpellID::FIRE_WALL: | ||||
| 			obstacle->obstacleType = CObstacleInstance::FIRE_WALL; | ||||
| 			obstacle->turnsRemaining = 2; | ||||
| 			obstacle->visibleForAnotherSide = true; | ||||
| 			break; | ||||
| 		case SpellID::FORCE_FIELD: | ||||
| 			obstacle->obstacleType = CObstacleInstance::FORCE_FIELD; | ||||
| 			obstacle->turnsRemaining = 2; | ||||
| 			obstacle->visibleForAnotherSide = true; | ||||
| 			break; | ||||
| 		default: | ||||
| 			//this function cannot be used with spells that do not create obstacles | ||||
| 			assert(0); | ||||
| 		} | ||||
|  | ||||
| 		obstacle->pos = pos; | ||||
| 		obstacle->casterSide = parameters.casterSide; | ||||
| 		obstacle->ID = owner->id; | ||||
| 		obstacle->spellLevel = parameters.spellLvl; | ||||
| 		obstacle->casterSpellPower = parameters.usedSpellPower; | ||||
| 		obstacle->uniqueID = obstacleIdToGive++; | ||||
|  | ||||
| 		BattleObstaclePlaced bop; | ||||
| 		bop.obstacle = obstacle; | ||||
| 		env->sendAndApply(&bop); | ||||
| 	};	 | ||||
| 	 | ||||
| 	switch(owner->id) | ||||
| 	{ | ||||
| 	case SpellID::QUICKSAND: | ||||
| 	case SpellID::LAND_MINE: | ||||
| 		{ | ||||
| 			std::vector<BattleHex> availableTiles; | ||||
| 			for(int i = 0; i < GameConstants::BFIELD_SIZE; i += 1) | ||||
| 			{ | ||||
| 				BattleHex hex = i; | ||||
| 				if(hex.getX() > 2 && hex.getX() < 14 && !(parameters.cb->battleGetStackByPos(hex, false)) && !(parameters.cb->battleGetObstacleOnPos(hex, false))) | ||||
| 					availableTiles.push_back(hex); | ||||
| 			} | ||||
| 			boost::range::random_shuffle(availableTiles); | ||||
|  | ||||
| 			const int patchesForSkill[] = {4, 4, 6, 8}; | ||||
| 			const int patchesToPut = std::min<int>(patchesForSkill[parameters.spellLvl], availableTiles.size()); | ||||
|  | ||||
| 			//land mines or quicksand patches are handled as spell created obstacles | ||||
| 			for (int i = 0; i < patchesToPut; i++) | ||||
| 				placeObstacle(availableTiles.at(i)); | ||||
| 		} | ||||
|  | ||||
| 		break; | ||||
| 	case SpellID::FORCE_FIELD: | ||||
| 		placeObstacle(parameters.destination); | ||||
| 		break; | ||||
| 	case SpellID::FIRE_WALL: | ||||
| 		{ | ||||
| 			//fire wall is build from multiple obstacles - one fire piece for each affected hex | ||||
| 			auto affectedHexes = owner->rangeInHexes(parameters.destination, parameters.spellLvl, parameters.casterSide); | ||||
| 			for(BattleHex hex : affectedHexes) | ||||
| 				placeObstacle(hex); | ||||
| 		} | ||||
| 		break; | ||||
| 	default:		 | ||||
| 		assert(0); | ||||
| 	}			 | ||||
| } | ||||
|  | ||||
|  | ||||
| ///WallMechanics | ||||
| std::vector<BattleHex> WallMechanics::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool * outDroppedHexes) const | ||||
| { | ||||
| 	std::vector<BattleHex> ret;	 | ||||
| 	 | ||||
| 	//Special case - shape of obstacle depends on caster's side | ||||
| 	//TODO make it possible through spell config | ||||
|  | ||||
| 	BattleHex::EDir firstStep, secondStep; | ||||
| 	if(side) | ||||
| 	{ | ||||
| 		firstStep = BattleHex::TOP_LEFT; | ||||
| 		secondStep = BattleHex::TOP_RIGHT; | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		firstStep = BattleHex::TOP_RIGHT; | ||||
| 		secondStep = BattleHex::TOP_LEFT; | ||||
| 	} | ||||
|  | ||||
| 	//Adds hex to the ret if it's valid. Otherwise sets output arg flag if given. | ||||
| 	auto addIfValid = [&](BattleHex hex) | ||||
| 	{ | ||||
| 		if(hex.isValid()) | ||||
| 			ret.push_back(hex); | ||||
| 		else if(outDroppedHexes) | ||||
| 			*outDroppedHexes = true; | ||||
| 	}; | ||||
|  | ||||
| 	ret.push_back(centralHex); | ||||
| 	addIfValid(centralHex.moveInDir(firstStep, false)); | ||||
| 	if(schoolLvl >= 2) //advanced versions of fire wall / force field cotnains of 3 hexes | ||||
| 		addIfValid(centralHex.moveInDir(secondStep, false)); //moveInDir function modifies subject hex | ||||
|  | ||||
| 	return ret;	 | ||||
| } | ||||
|  | ||||
| ///RemoveObstacleMechanics | ||||
| void RemoveObstacleMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const | ||||
| { | ||||
| 	if(auto obstacleToRemove = parameters.cb->battleGetObstacleOnPos(parameters.destination, false)) | ||||
| 	{ | ||||
| 		ObstaclesRemoved obr; | ||||
| 		obr.obstacles.insert(obstacleToRemove->uniqueID); | ||||
| 		env->sendAndApply(&obr); | ||||
| 	} | ||||
| 	else | ||||
| 		env->complain("There's no obstacle to remove!");	 | ||||
| } | ||||
|  | ||||
| ///SpecialRisingSpellMechanics | ||||
| void SacrificeMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const | ||||
| { | ||||
| 	RisingSpellMechanics::applyBattleEffects(env, parameters, ctx); | ||||
|  | ||||
| 	if(parameters.selectedStack == parameters.cb->battleActiveStack()) | ||||
| 	//set another active stack than the one removed, or bad things will happen | ||||
| 	//TODO: make that part of BattleStacksRemoved? what about client update? | ||||
| 	{ | ||||
| 		//makeStackDoNothing(gs->curB->getStack (selectedStack)); | ||||
|  | ||||
| 		BattleSetActiveStack sas; | ||||
|  | ||||
| 		//std::vector<const CStack *> hlp; | ||||
| 		//battleGetStackQueue(hlp, 1, selectedStack); //next after this one | ||||
|  | ||||
| 		//if(hlp.size()) | ||||
| 		//{ | ||||
| 		//	sas.stack = hlp[0]->ID; | ||||
| 		//} | ||||
| 		//else | ||||
| 		//	complain ("No new stack to activate!"); | ||||
| 		sas.stack = parameters.cb->getNextStack()->ID; //why the hell next stack has same ID as current? | ||||
| 		env->sendAndApply(&sas); | ||||
|  | ||||
| 	} | ||||
| 	BattleStacksRemoved bsr; | ||||
| 	bsr.stackIDs.insert(parameters.selectedStack->ID); //somehow it works for teleport? | ||||
| 	env->sendAndApply(&bsr); | ||||
| 		 | ||||
| } | ||||
|  | ||||
|  | ||||
| ///SpecialRisingSpellMechanics | ||||
| ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const | ||||
| { | ||||
| 	// following does apply to resurrect and animate dead(?) only | ||||
| 	// for sacrifice health calculation and health limit check don't matter | ||||
|  | ||||
| 	if(obj->count >= obj->baseAmount) | ||||
| 		return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; | ||||
| 	 | ||||
| 	if(caster) //FIXME: Archangels can cast immune stack | ||||
| 	{ | ||||
| 		auto maxHealth = calculateHealedHP(caster, obj, nullptr); | ||||
| 		if (maxHealth < obj->MaxHealth()) //must be able to rise at least one full creature | ||||
| 			return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; | ||||
| 	}	 | ||||
| 	 | ||||
| 	return DefaultSpellMechanics::isImmuneByStack(caster,obj);	 | ||||
| } | ||||
|  | ||||
| ///SummonMechanics | ||||
| void SummonMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const | ||||
| { | ||||
| 	//todo: make configurable | ||||
| 	CreatureID creID = CreatureID::NONE; | ||||
| 	switch(owner->id) | ||||
| 	{ | ||||
| 		case SpellID::SUMMON_FIRE_ELEMENTAL: | ||||
| 			creID = CreatureID::FIRE_ELEMENTAL; | ||||
| 			break; | ||||
| 		case SpellID::SUMMON_EARTH_ELEMENTAL: | ||||
| 			creID = CreatureID::EARTH_ELEMENTAL; | ||||
| 			break; | ||||
| 		case SpellID::SUMMON_WATER_ELEMENTAL: | ||||
| 			creID = CreatureID::WATER_ELEMENTAL; | ||||
| 			break; | ||||
| 		case SpellID::SUMMON_AIR_ELEMENTAL: | ||||
| 			creID = CreatureID::AIR_ELEMENTAL; | ||||
| 			break; | ||||
| 		default: | ||||
| 			env->complain("Unable to determine summoned creature"); | ||||
| 			return; | ||||
| 	} | ||||
|  | ||||
| 	BattleStackAdded bsa; | ||||
| 	bsa.creID = creID; | ||||
| 	bsa.attacker = !(bool)parameters.casterSide; | ||||
| 	bsa.summoned = true; | ||||
| 	bsa.pos = parameters.cb->getAvaliableHex(creID, !(bool)parameters.casterSide); //TODO: unify it | ||||
|  | ||||
| 	//TODO stack casting -> probably power will be zero; set the proper number of creatures manually | ||||
| 	int percentBonus = parameters.caster ? parameters.caster->valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, owner->id.toEnum()) : 0; | ||||
|  | ||||
| 	bsa.amount = parameters.usedSpellPower | ||||
| 		* owner->getPower(parameters.spellLvl) | ||||
| 		* (100 + percentBonus) / 100.0; //new feature - percentage bonus | ||||
| 	if(bsa.amount) | ||||
| 		env->sendAndApply(&bsa); | ||||
| 	else | ||||
| 		env->complain("Summoning didn't summon any!");	 | ||||
| } | ||||
|  | ||||
|  | ||||
| ///TeleportMechanics | ||||
| void TeleportMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const | ||||
| { | ||||
| 	BattleStackMoved bsm; | ||||
| 	bsm.distance = -1; | ||||
| 	bsm.stack = parameters.selectedStack->ID; | ||||
| 	std::vector<BattleHex> tiles; | ||||
| 	tiles.push_back(parameters.destination); | ||||
| 	bsm.tilesToMove = tiles; | ||||
| 	bsm.teleporting = true; | ||||
| 	env->sendAndApply(&bsm);	 | ||||
| } | ||||
| 	 | ||||
|   | ||||
| @@ -11,3 +11,109 @@ | ||||
|  #pragma once | ||||
|   | ||||
|  #include "CDefaultSpellMechanics.h" | ||||
|  | ||||
|  | ||||
| class ChainLightningMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	ChainLightningMechanics(CSpell * s): DefaultSpellMechanics(s){};	 | ||||
| 	std::set<const CStack *> getAffectedStacks(SpellTargetingContext & ctx) const override; | ||||
| }; | ||||
|  | ||||
| class CloneMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	CloneMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| 	ESpellCastProblem::ESpellCastProblem isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const override; | ||||
| protected: | ||||
| 	void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override;	 | ||||
| }; | ||||
|  | ||||
| class CureMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	CureMechanics(CSpell * s): DefaultSpellMechanics(s){};	 | ||||
| 	 | ||||
| 	void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const override;	 | ||||
| }; | ||||
|  | ||||
|  | ||||
|  | ||||
| class DispellMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	DispellMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| 	 | ||||
| 	void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const override;	 | ||||
| }; | ||||
|  | ||||
| class HypnotizeMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	HypnotizeMechanics(CSpell * s): DefaultSpellMechanics(s){};	 | ||||
| 	ESpellCastProblem::ESpellCastProblem isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const override;	 | ||||
| };  | ||||
|  | ||||
| class ObstacleMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	ObstacleMechanics(CSpell * s): DefaultSpellMechanics(s){};		 | ||||
|  | ||||
| protected: | ||||
| 	void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override;	 | ||||
| }; | ||||
|  | ||||
| class WallMechanics: public ObstacleMechanics | ||||
| { | ||||
| public: | ||||
| 	WallMechanics(CSpell * s): ObstacleMechanics(s){};	 | ||||
| 	std::vector<BattleHex> rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes = nullptr) const override;		 | ||||
| }; | ||||
|  | ||||
| class RemoveObstacleMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	RemoveObstacleMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| protected: | ||||
| 	void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override;		 | ||||
| }; | ||||
|  | ||||
| ///all rising spells | ||||
| class RisingSpellMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	RisingSpellMechanics(CSpell * s): DefaultSpellMechanics(s){};		 | ||||
| 	 | ||||
| }; | ||||
|  | ||||
| class SacrificeMechanics: public RisingSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	SacrificeMechanics(CSpell * s): RisingSpellMechanics(s){};	 | ||||
| protected: | ||||
| 	void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override;		 | ||||
| }; | ||||
|  | ||||
| ///all rising spells but SACRIFICE | ||||
| class SpecialRisingSpellMechanics: public RisingSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	SpecialRisingSpellMechanics(CSpell * s): RisingSpellMechanics(s){}; | ||||
| 	ESpellCastProblem::ESpellCastProblem isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const override;						 | ||||
| }; | ||||
|  | ||||
| class SummonMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	SummonMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| protected: | ||||
| 	void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override;		 | ||||
| }; | ||||
|  | ||||
| class TeleportMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	TeleportMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| protected: | ||||
| 	void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override;		 | ||||
| }; | ||||
|   | ||||
| @@ -11,3 +11,694 @@ | ||||
| #include "StdInc.h" | ||||
|  | ||||
| #include "CDefaultSpellMechanics.h" | ||||
|  | ||||
| #include "../NetPacks.h" | ||||
| #include "../BattleState.h" | ||||
|  | ||||
| namespace SRSLPraserHelpers | ||||
| { | ||||
| 	static int XYToHex(int x, int y) | ||||
| 	{ | ||||
| 		return x + GameConstants::BFIELD_WIDTH * y; | ||||
| 	} | ||||
|  | ||||
| 	static int XYToHex(std::pair<int, int> xy) | ||||
| 	{ | ||||
| 		return XYToHex(xy.first, xy.second); | ||||
| 	} | ||||
|  | ||||
| 	static int hexToY(int battleFieldPosition) | ||||
| 	{ | ||||
| 		return battleFieldPosition/GameConstants::BFIELD_WIDTH; | ||||
| 	} | ||||
|  | ||||
| 	static int hexToX(int battleFieldPosition) | ||||
| 	{ | ||||
| 		int pos = battleFieldPosition - hexToY(battleFieldPosition) * GameConstants::BFIELD_WIDTH; | ||||
| 		return pos; | ||||
| 	} | ||||
|  | ||||
| 	static std::pair<int, int> hexToPair(int battleFieldPosition) | ||||
| 	{ | ||||
| 		return std::make_pair(hexToX(battleFieldPosition), hexToY(battleFieldPosition)); | ||||
| 	} | ||||
|  | ||||
| 	//moves hex by one hex in given direction | ||||
| 	//0 - left top, 1 - right top, 2 - right, 3 - right bottom, 4 - left bottom, 5 - left | ||||
| 	static std::pair<int, int> gotoDir(int x, int y, int direction) | ||||
| 	{ | ||||
| 		switch(direction) | ||||
| 		{ | ||||
| 		case 0: //top left | ||||
| 			return std::make_pair((y%2) ? x-1 : x, y-1); | ||||
| 		case 1: //top right | ||||
| 			return std::make_pair((y%2) ? x : x+1, y-1); | ||||
| 		case 2:  //right | ||||
| 			return std::make_pair(x+1, y); | ||||
| 		case 3: //right bottom | ||||
| 			return std::make_pair((y%2) ? x : x+1, y+1); | ||||
| 		case 4: //left bottom | ||||
| 			return std::make_pair((y%2) ? x-1 : x, y+1); | ||||
| 		case 5: //left | ||||
| 			return std::make_pair(x-1, y); | ||||
| 		default: | ||||
| 			throw std::runtime_error("Disaster: wrong direction in SRSLPraserHelpers::gotoDir!\n"); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	static std::pair<int, int> gotoDir(std::pair<int, int> xy, int direction) | ||||
| 	{ | ||||
| 		return gotoDir(xy.first, xy.second, direction); | ||||
| 	} | ||||
|  | ||||
| 	static bool isGoodHex(std::pair<int, int> xy) | ||||
| 	{ | ||||
| 		return xy.first >=0 && xy.first < GameConstants::BFIELD_WIDTH && xy.second >= 0 && xy.second < GameConstants::BFIELD_HEIGHT; | ||||
| 	} | ||||
|  | ||||
| 	//helper function for rangeInHexes | ||||
| 	static std::set<ui16> getInRange(unsigned int center, int low, int high) | ||||
| 	{ | ||||
| 		std::set<ui16> ret; | ||||
| 		if(low == 0) | ||||
| 		{ | ||||
| 			ret.insert(center); | ||||
| 		} | ||||
|  | ||||
| 		std::pair<int, int> mainPointForLayer[6]; //A, B, C, D, E, F points | ||||
| 		for(auto & elem : mainPointForLayer) | ||||
| 			elem = hexToPair(center); | ||||
|  | ||||
| 		for(int it=1; it<=high; ++it) //it - distance to the center | ||||
| 		{ | ||||
| 			for(int b=0; b<6; ++b) | ||||
| 				mainPointForLayer[b] = gotoDir(mainPointForLayer[b], b); | ||||
|  | ||||
| 			if(it>=low) | ||||
| 			{ | ||||
| 				std::pair<int, int> curHex; | ||||
|  | ||||
| 				//adding lines (A-b, B-c, C-d, etc) | ||||
| 				for(int v=0; v<6; ++v) | ||||
| 				{ | ||||
| 					curHex = mainPointForLayer[v]; | ||||
| 					for(int h=0; h<it; ++h) | ||||
| 					{ | ||||
| 						if(isGoodHex(curHex)) | ||||
| 							ret.insert(XYToHex(curHex)); | ||||
| 						curHex = gotoDir(curHex, (v+2)%6); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 			} //if(it>=low) | ||||
| 		} | ||||
|  | ||||
| 		return ret; | ||||
| 	} | ||||
| } | ||||
|  | ||||
|  | ||||
| ///DefaultSpellMechanics | ||||
| void DefaultSpellMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const | ||||
| { | ||||
| 	if (packet->castedByHero) | ||||
| 	{ | ||||
| 		if (packet->side < 2) | ||||
| 		{ | ||||
| 			battle->sides[packet->side].castSpellsCount++; | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	//handle countering spells | ||||
| 	for(auto stackID : packet->affectedCres) | ||||
| 	{ | ||||
| 		if(vstd::contains(packet->resisted, stackID)) | ||||
| 			continue; | ||||
|  | ||||
| 		CStack * s = battle->getStack(stackID); | ||||
| 		s->popBonuses([&](const Bonus * b) -> bool | ||||
| 		{ | ||||
| 			//check for each bonus if it should be removed | ||||
| 			const bool isSpellEffect = Selector::sourceType(Bonus::SPELL_EFFECT)(b); | ||||
| 			const int spellID = isSpellEffect ? b->sid : -1; | ||||
|  | ||||
| 			return isSpellEffect && vstd::contains(owner->counteredSpells, spellID); | ||||
| 		}); | ||||
| 	}	 | ||||
| } | ||||
|  | ||||
| bool DefaultSpellMechanics::adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const | ||||
| { | ||||
| 	if(!owner->isAdventureSpell()) | ||||
| 	{ | ||||
| 		env->complain("Attempt to cast non adventure spell in adventure mode"); | ||||
| 		return false; | ||||
| 	} | ||||
| 	 | ||||
| 	const CGHeroInstance * caster = parameters.caster; | ||||
| 	const int cost = caster->getSpellCost(owner); | ||||
| 	 | ||||
| 	if(!caster->canCastThisSpell(owner)) | ||||
| 	{ | ||||
| 		env->complain("Hero cannot cast this spell!"); | ||||
| 		return false;		 | ||||
| 	} | ||||
|  | ||||
| 	if(caster->mana < cost) | ||||
| 	{ | ||||
| 		env->complain("Hero doesn't have enough spell points to cast this spell!"); | ||||
| 		return false;		 | ||||
| 	} | ||||
|  | ||||
| 	{ | ||||
| 		AdvmapSpellCast asc; | ||||
| 		asc.caster = caster; | ||||
| 		asc.spellID = owner->id; | ||||
| 		env->sendAndApply(&asc);		 | ||||
| 	} | ||||
| 	 | ||||
| 	if(applyAdventureEffects(env, parameters)) | ||||
| 	{ | ||||
| 		SetMana sm; | ||||
| 		sm.hid = caster->id; | ||||
| 		sm.absolute = false; | ||||
| 		sm.val = -cost; | ||||
| 		env->sendAndApply(&sm); | ||||
| 		return true; | ||||
| 	} | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| bool DefaultSpellMechanics::applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const | ||||
| { | ||||
| 	//There is no generic algorithm of adventure cast | ||||
| 	env->complain("Unimplemented adventure spell"); | ||||
| 	return false;		 | ||||
| } | ||||
|  | ||||
|  | ||||
| void DefaultSpellMechanics::battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const | ||||
| { | ||||
| 	BattleSpellCast sc; | ||||
| 	sc.side = parameters.casterSide; | ||||
| 	sc.id = owner->id; | ||||
| 	sc.skill = parameters.spellLvl; | ||||
| 	sc.tile = parameters.destination; | ||||
| 	sc.dmgToDisplay = 0; | ||||
| 	sc.castedByHero = nullptr != parameters.caster; | ||||
| 	sc.casterStack = (parameters.casterStack ? parameters.casterStack->ID : -1); | ||||
| 	sc.manaGained = 0; | ||||
| 	 | ||||
| 	int spellCost = 0;	 | ||||
| 	 | ||||
| 	//calculate spell cost | ||||
| 	if(parameters.caster)  | ||||
| 	{ | ||||
| 		spellCost = parameters.cb->battleGetSpellCost(owner, parameters.caster); | ||||
|  | ||||
| 		if(parameters.secHero && parameters.mode == ECastingMode::HERO_CASTING) //handle mana channel | ||||
| 		{ | ||||
| 			int manaChannel = 0; | ||||
| 			for(const CStack * stack : parameters.cb->battleGetAllStacks(true)) //TODO: shouldn't bonus system handle it somehow? | ||||
| 			{ | ||||
| 				if(stack->owner == parameters.secHero->tempOwner) | ||||
| 				{ | ||||
| 					vstd::amax(manaChannel, stack->valOfBonuses(Bonus::MANA_CHANNELING)); | ||||
| 				} | ||||
| 			} | ||||
| 			sc.manaGained = (manaChannel * spellCost) / 100; | ||||
| 		} | ||||
| 	}	 | ||||
| 	 | ||||
| 	 | ||||
| 	//calculating affected creatures for all spells | ||||
| 	//must be vector, as in Chain Lightning order matters | ||||
| 	std::vector<const CStack*> attackedCres; //CStack vector is somewhat more suitable than ID vector | ||||
|  | ||||
| 	auto creatures = owner->getAffectedStacks(parameters.cb, parameters.mode, parameters.casterColor, parameters.spellLvl, parameters.destination, parameters.caster); | ||||
| 	std::copy(creatures.begin(), creatures.end(), std::back_inserter(attackedCres)); | ||||
| 	 | ||||
| 	for (auto cre : attackedCres) | ||||
| 	{ | ||||
| 		sc.affectedCres.insert(cre->ID); | ||||
| 	} | ||||
| 	 | ||||
| 	//checking if creatures resist | ||||
| 	//resistance is applied only to negative spells | ||||
| 	if(owner->isNegative()) | ||||
| 	{ | ||||
| 		for(auto s : attackedCres) | ||||
| 		{ | ||||
| 			const int prob = std::min((s)->magicResistance(), 100); //probability of resistance in % | ||||
| 			 | ||||
| 			if(env->getRandomGenerator().nextInt(99) < prob) | ||||
| 			{ | ||||
| 				sc.resisted.push_back(s->ID); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	StacksInjured si;	 | ||||
| 	SpellCastContext ctx(attackedCres, sc, si); | ||||
| 	 | ||||
| 	applyBattleEffects(env, parameters, ctx); | ||||
| 	 | ||||
| 	env->sendAndApply(&sc); | ||||
| 	 | ||||
|  | ||||
| 	//spend mana | ||||
| 	if(parameters.caster)  | ||||
| 	{ | ||||
| 		SetMana sm; | ||||
| 		sm.absolute = false; | ||||
| 		 | ||||
| 		sm.hid = parameters.caster->id; | ||||
| 		sm.val = -spellCost; | ||||
| 		 | ||||
| 		env->sendAndApply(&sm); | ||||
| 		 | ||||
| 		if(sc.manaGained > 0) | ||||
| 		{ | ||||
| 			assert(parameters.secHero); | ||||
| 			 | ||||
| 			sm.hid = parameters.secHero->id; | ||||
| 			sm.val = sc.manaGained; | ||||
| 			env->sendAndApply(&sm); | ||||
| 		}		 | ||||
| 	} | ||||
| 	 | ||||
| 	if(!si.stacks.empty()) //after spellcast info shows | ||||
| 		env->sendAndApply(&si); | ||||
| 	 | ||||
| 	//reduce number of casts remaining | ||||
| 	//TODO: this should be part of BattleSpellCast apply | ||||
| 	if (parameters.mode == ECastingMode::CREATURE_ACTIVE_CASTING || parameters.mode == ECastingMode::ENCHANTER_CASTING)  | ||||
| 	{ | ||||
| 		assert(parameters.casterStack); | ||||
| 		 | ||||
| 		BattleSetStackProperty ssp; | ||||
| 		ssp.stackID = parameters.casterStack->ID; | ||||
| 		ssp.which = BattleSetStackProperty::CASTS; | ||||
| 		ssp.val = -1; | ||||
| 		ssp.absolute = false; | ||||
| 		env->sendAndApply(&ssp); | ||||
| 	} | ||||
|  | ||||
| 	//Magic Mirror effect | ||||
| 	if(owner->isNegative() && parameters.mode != ECastingMode::MAGIC_MIRROR && owner->level && owner->getLevelInfo(0).range == "0") //it is actual spell and can be reflected to single target, no recurrence | ||||
| 	{ | ||||
| 		for(auto & attackedCre : attackedCres) | ||||
| 		{ | ||||
| 			int mirrorChance = (attackedCre)->valOfBonuses(Bonus::MAGIC_MIRROR); | ||||
| 			if(mirrorChance > env->getRandomGenerator().nextInt(99)) | ||||
| 			{ | ||||
| 				std::vector<const CStack *> mirrorTargets; | ||||
| 				auto battleStacks = parameters.cb->battleGetAllStacks(true); | ||||
| 				for(auto & battleStack : battleStacks) | ||||
| 				{ | ||||
| 					if(battleStack->owner == parameters.casterColor) //get enemy stacks which can be affected by this spell | ||||
| 					{ | ||||
| 						if (ESpellCastProblem::OK == owner->isImmuneByStack(nullptr, battleStack)) | ||||
| 							mirrorTargets.push_back(battleStack); | ||||
| 					} | ||||
| 				} | ||||
| 				if(!mirrorTargets.empty()) | ||||
| 				{ | ||||
| 					int targetHex = (*RandomGeneratorUtil::nextItem(mirrorTargets, env->getRandomGenerator()))->position; | ||||
| 					 | ||||
| 					BattleSpellCastParameters mirrorParameters = parameters; | ||||
| 					mirrorParameters.spellLvl = 0; | ||||
| 					mirrorParameters.casterSide = 1-parameters.casterSide; | ||||
| 					mirrorParameters.casterColor = (attackedCre)->owner; | ||||
| 					mirrorParameters.caster = nullptr; | ||||
| 					mirrorParameters.destination = targetHex; | ||||
| 					mirrorParameters.secHero = parameters.caster; | ||||
| 					mirrorParameters.mode = ECastingMode::MAGIC_MIRROR; | ||||
| 					mirrorParameters.casterStack = (attackedCre); | ||||
| 					mirrorParameters.selectedStack = nullptr; | ||||
| 					 | ||||
| 					battleCast(env, mirrorParameters);					 | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}	 | ||||
| } | ||||
|  | ||||
| int DefaultSpellMechanics::calculateDuration(const CGHeroInstance * caster, int usedSpellPower) const | ||||
| { | ||||
| 	if(!caster) | ||||
| 	{ | ||||
| 		if (!usedSpellPower) | ||||
| 			return 3; //default duration of all creature spells | ||||
| 		else | ||||
| 			return usedSpellPower; //use creature spell power | ||||
| 	} | ||||
| 	switch(owner->id) | ||||
| 	{ | ||||
| 	case SpellID::FRENZY: | ||||
| 		return 1; | ||||
| 	default: //other spells | ||||
| 		return caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER) + caster->valOfBonuses(Bonus::SPELL_DURATION); | ||||
| 	}	 | ||||
| } | ||||
|  | ||||
| ui32 DefaultSpellMechanics::calculateHealedHP(const CGHeroInstance* caster, const CStack* stack, const CStack* sacrificedStack) const | ||||
| { | ||||
| 	int healedHealth; | ||||
| 	 | ||||
| 	if(!owner->isHealingSpell()) | ||||
| 	{ | ||||
| 		logGlobal->errorStream() << "calculateHealedHP called for nonhealing spell "<< owner->name; | ||||
| 		return 0; | ||||
| 	}		 | ||||
| 	 | ||||
| 	const int spellPowerSkill = caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER); | ||||
| 	const int levelPower = owner->getPower(caster->getSpellSchoolLevel(owner)); | ||||
| 	 | ||||
| 	if (owner->id == SpellID::SACRIFICE && sacrificedStack) | ||||
| 		healedHealth = (spellPowerSkill + sacrificedStack->MaxHealth() + levelPower) * sacrificedStack->count; | ||||
| 	else | ||||
| 		healedHealth = spellPowerSkill * owner->power + levelPower; //??? | ||||
| 	healedHealth = owner->calculateBonus(healedHealth, caster, stack); | ||||
| 	return std::min<ui32>(healedHealth, stack->MaxHealth() - stack->firstHPleft + (owner->isRisingSpell() ? stack->baseAmount * stack->MaxHealth() : 0));		 | ||||
| } | ||||
|  | ||||
|  | ||||
| void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const | ||||
| { | ||||
| 	//applying effects | ||||
| 	if(owner->isOffensiveSpell()) | ||||
| 	{ | ||||
| 		int spellDamage = 0; | ||||
| 		if(parameters.casterStack && parameters.mode != ECastingMode::MAGIC_MIRROR) | ||||
| 		{ | ||||
| 			int unitSpellPower = parameters.casterStack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, owner->id.toEnum()); | ||||
| 			if(unitSpellPower) | ||||
| 				ctx.sc.dmgToDisplay = spellDamage = parameters.casterStack->count * unitSpellPower; //TODO: handle immunities | ||||
| 			else //Faerie Dragon | ||||
| 			{ | ||||
| 				parameters.usedSpellPower = parameters.casterStack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * parameters.casterStack->count / 100; | ||||
| 				ctx.sc.dmgToDisplay = 0; | ||||
| 			} | ||||
| 		} | ||||
| 		int chainLightningModifier = 0; | ||||
| 		for(auto & attackedCre : ctx.attackedCres) | ||||
| 		{ | ||||
| 			if(vstd::contains(ctx.sc.resisted, (attackedCre)->ID)) //this creature resisted the spell | ||||
| 				continue; | ||||
|  | ||||
| 			BattleStackAttacked bsa; | ||||
| 			if(spellDamage) | ||||
| 				bsa.damageAmount = spellDamage >> chainLightningModifier; | ||||
| 			else | ||||
| 				bsa.damageAmount =  owner->calculateDamage(parameters.caster, attackedCre, parameters.spellLvl, parameters.usedSpellPower) >> chainLightningModifier; | ||||
|  | ||||
| 			ctx.sc.dmgToDisplay += bsa.damageAmount; | ||||
|  | ||||
| 			bsa.stackAttacked = (attackedCre)->ID; | ||||
| 			if(parameters.mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast | ||||
| 				bsa.attackerID = parameters.casterStack->ID; | ||||
| 			else | ||||
| 				bsa.attackerID = -1; | ||||
| 			(attackedCre)->prepareAttacked(bsa, env->getRandomGenerator()); | ||||
| 			ctx.si.stacks.push_back(bsa); | ||||
|  | ||||
| 			if(owner->id == SpellID::CHAIN_LIGHTNING) | ||||
| 				++chainLightningModifier; | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	if(owner->hasEffects()) | ||||
| 	{ | ||||
| 		int stackSpellPower = 0; | ||||
| 		if(parameters.casterStack && parameters.mode != ECastingMode::MAGIC_MIRROR) | ||||
| 		{ | ||||
| 			stackSpellPower =  parameters.casterStack->valOfBonuses(Bonus::CREATURE_ENCHANT_POWER); | ||||
| 		} | ||||
| 		SetStackEffect sse; | ||||
| 		Bonus pseudoBonus; | ||||
| 		pseudoBonus.sid = owner->id; | ||||
| 		pseudoBonus.val = parameters.spellLvl; | ||||
| 		pseudoBonus.turnsRemain = calculateDuration(parameters.caster, stackSpellPower ? stackSpellPower : parameters.usedSpellPower); | ||||
| 		CStack::stackEffectToFeature(sse.effect, pseudoBonus); | ||||
| 		if(owner->id == SpellID::SHIELD || owner->id == SpellID::AIR_SHIELD) | ||||
| 		{ | ||||
| 			sse.effect.back().val = (100 - sse.effect.back().val); //fix to original config: shield should display damage reduction | ||||
| 		} | ||||
| 		if(owner->id == SpellID::BIND &&  parameters.casterStack)//bind | ||||
| 		{ | ||||
| 			sse.effect.back().additionalInfo =  parameters.casterStack->ID; //we need to know who casted Bind | ||||
| 		} | ||||
| 		const Bonus * bonus = nullptr; | ||||
| 		if(parameters.caster) | ||||
| 			bonus = parameters.caster->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPECIAL_PECULIAR_ENCHANT, owner->id)); | ||||
| 		//TODO does hero specialty should affects his stack casting spells? | ||||
|  | ||||
| 		si32 power = 0; | ||||
| 		for(const CStack * affected : ctx.attackedCres) | ||||
| 		{ | ||||
| 			if(vstd::contains(ctx.sc.resisted, affected->ID)) //this creature resisted the spell | ||||
| 				continue; | ||||
| 			sse.stacks.push_back(affected->ID); | ||||
|  | ||||
| 			//Apply hero specials - peculiar enchants | ||||
| 			const ui8 tier = std::max((ui8)1, affected->getCreature()->level); //don't divide by 0 for certain creatures (commanders, war machines) | ||||
| 			if(bonus) | ||||
| 			{ | ||||
| 				switch(bonus->additionalInfo) | ||||
| 				{ | ||||
| 					case 0: //normal | ||||
| 					{ | ||||
| 						switch(tier) | ||||
| 						{ | ||||
| 							case 1: case 2: | ||||
| 								power = 3; | ||||
| 							break; | ||||
| 							case 3: case 4: | ||||
| 								power = 2; | ||||
| 							break; | ||||
| 							case 5: case 6: | ||||
| 								power = 1; | ||||
| 							break; | ||||
| 						} | ||||
| 						Bonus specialBonus(sse.effect.back()); | ||||
| 						specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely | ||||
| 						sse.uniqueBonuses.push_back (std::pair<ui32,Bonus> (affected->ID, specialBonus)); //additional premy to given effect | ||||
| 					} | ||||
| 					break; | ||||
| 					case 1: //only Coronius as yet | ||||
| 					{ | ||||
| 						power = std::max(5 - tier, 0); | ||||
| 						Bonus specialBonus = CStack::featureGenerator(Bonus::PRIMARY_SKILL, PrimarySkill::ATTACK, power, pseudoBonus.turnsRemain); | ||||
| 						specialBonus.sid = owner->id; | ||||
| 						sse.uniqueBonuses.push_back(std::pair<ui32,Bonus> (affected->ID, specialBonus)); //additional attack to Slayer effect | ||||
| 					} | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 			if (parameters.caster && parameters.caster->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, owner->id)) //TODO: better handling of bonus percentages | ||||
| 			{ | ||||
| 				int damagePercent = parameters.caster->level * parameters.caster->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, owner->id.toEnum()) / tier; | ||||
| 				Bonus specialBonus = CStack::featureGenerator(Bonus::CREATURE_DAMAGE, 0, damagePercent, pseudoBonus.turnsRemain); | ||||
| 				specialBonus.valType = Bonus::PERCENT_TO_ALL; | ||||
| 				specialBonus.sid = owner->id; | ||||
| 				sse.uniqueBonuses.push_back (std::pair<ui32,Bonus> (affected->ID, specialBonus)); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if(!sse.stacks.empty()) | ||||
| 			env->sendAndApply(&sse); | ||||
|  | ||||
| 	} | ||||
| 	 | ||||
| 	if(owner->isHealingSpell()) | ||||
| 	{ | ||||
| 		int hpGained = 0; | ||||
| 		if(parameters.casterStack) | ||||
| 		{ | ||||
| 			int unitSpellPower = parameters.casterStack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, owner->id.toEnum()); | ||||
| 			if(unitSpellPower) | ||||
| 				hpGained = parameters.casterStack->count * unitSpellPower; //Archangel | ||||
| 			else //Faerie Dragon-like effect - unused so far | ||||
| 				parameters.usedSpellPower = parameters.casterStack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * parameters.casterStack->count / 100; | ||||
| 		} | ||||
| 		StacksHealedOrResurrected shr; | ||||
| 		shr.lifeDrain = false; | ||||
| 		shr.tentHealing = false; | ||||
| 		for(auto & attackedCre : ctx.attackedCres) | ||||
| 		{ | ||||
| 			StacksHealedOrResurrected::HealInfo hi; | ||||
| 			hi.stackID = (attackedCre)->ID; | ||||
| 			if (parameters.casterStack) //casted by creature | ||||
| 			{ | ||||
| 				const bool resurrect = owner->isRisingSpell(); | ||||
| 				if (hpGained) | ||||
| 				{ | ||||
| 					//archangel | ||||
| 					hi.healedHP = std::min<ui32>(hpGained, attackedCre->MaxHealth() - attackedCre->firstHPleft + (resurrect ? attackedCre->baseAmount * attackedCre->MaxHealth() : 0)); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					//any typical spell (commander's cure or animate dead) | ||||
| 					int healedHealth = parameters.usedSpellPower * owner->power + owner->getPower(parameters.spellLvl); | ||||
| 					hi.healedHP = std::min<ui32>(healedHealth, attackedCre->MaxHealth() - attackedCre->firstHPleft + (resurrect ? attackedCre->baseAmount * attackedCre->MaxHealth() : 0)); | ||||
| 				}					 | ||||
| 			} | ||||
| 			else | ||||
| 				hi.healedHP = calculateHealedHP(parameters.caster, attackedCre, parameters.selectedStack); //Casted by hero | ||||
| 			hi.lowLevelResurrection = parameters.spellLvl <= 1; | ||||
| 			shr.healedStacks.push_back(hi); | ||||
| 		} | ||||
| 		if(!shr.healedStacks.empty()) | ||||
| 			env->sendAndApply(&shr); | ||||
| 	}		 | ||||
| } | ||||
|  | ||||
|  | ||||
| std::vector<BattleHex> DefaultSpellMechanics::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const | ||||
| { | ||||
| 	using namespace SRSLPraserHelpers; | ||||
| 	 | ||||
| 	std::vector<BattleHex> ret; | ||||
| 	std::string rng = owner->getLevelInfo(schoolLvl).range + ','; //copy + artificial comma for easier handling | ||||
|  | ||||
| 	if(rng.size() >= 2 && rng[0] != 'X') //there is at lest one hex in range (+artificial comma) | ||||
| 	{ | ||||
| 		std::string number1, number2; | ||||
| 		int beg, end; | ||||
| 		bool readingFirst = true; | ||||
| 		for(auto & elem : rng) | ||||
| 		{ | ||||
| 			if(std::isdigit(elem) ) //reading number | ||||
| 			{ | ||||
| 				if(readingFirst) | ||||
| 					number1 += elem; | ||||
| 				else | ||||
| 					number2 += elem; | ||||
| 			} | ||||
| 			else if(elem == ',') //comma | ||||
| 			{ | ||||
| 				//calculating variables | ||||
| 				if(readingFirst) | ||||
| 				{ | ||||
| 					beg = atoi(number1.c_str()); | ||||
| 					number1 = ""; | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					end = atoi(number2.c_str()); | ||||
| 					number2 = ""; | ||||
| 				} | ||||
| 				//obtaining new hexes | ||||
| 				std::set<ui16> curLayer; | ||||
| 				if(readingFirst) | ||||
| 				{ | ||||
| 					curLayer = getInRange(centralHex, beg, beg); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					curLayer = getInRange(centralHex, beg, end); | ||||
| 					readingFirst = true; | ||||
| 				} | ||||
| 				//adding abtained hexes | ||||
| 				for(auto & curLayer_it : curLayer) | ||||
| 				{ | ||||
| 					ret.push_back(curLayer_it); | ||||
| 				} | ||||
|  | ||||
| 			} | ||||
| 			else if(elem == '-') //dash | ||||
| 			{ | ||||
| 				beg = atoi(number1.c_str()); | ||||
| 				number1 = ""; | ||||
| 				readingFirst = false; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	//remove duplicates (TODO check if actually needed) | ||||
| 	range::unique(ret); | ||||
| 	return ret;		 | ||||
| } | ||||
|  | ||||
|  | ||||
| std::set<const CStack *> DefaultSpellMechanics::getAffectedStacks(SpellTargetingContext & ctx) const | ||||
| { | ||||
| 	std::set<const CStack* > attackedCres;//std::set to exclude multiple occurrences of two hex creatures | ||||
| 	 | ||||
| 	const ui8 attackerSide = ctx.cb->playerToSide(ctx.casterColor) == 1; | ||||
| 	const auto attackedHexes = rangeInHexes(ctx.destination, ctx.schoolLvl, attackerSide); | ||||
|  | ||||
| 	const CSpell::TargetInfo ti(owner, ctx.schoolLvl, ctx.mode); | ||||
| 	 | ||||
| 	//TODO: more generic solution for mass spells | ||||
| 	if(owner->getLevelInfo(ctx.schoolLvl).range.size() > 1) //custom many-hex range | ||||
| 	{ | ||||
| 		for(BattleHex hex : attackedHexes) | ||||
| 		{ | ||||
| 			if(const CStack * st = ctx.cb->battleGetStackByPos(hex, ti.onlyAlive)) | ||||
| 			{ | ||||
| 				attackedCres.insert(st); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	else if(ti.type == CSpell::CREATURE) | ||||
| 	{ | ||||
| 		auto predicate = [=](const CStack * s){ | ||||
| 			const bool positiveToAlly = owner->isPositive() && s->owner == ctx.casterColor; | ||||
| 			const bool negativeToEnemy = owner->isNegative() && s->owner != ctx.casterColor; | ||||
| 			const bool validTarget = s->isValidTarget(!ti.onlyAlive); //todo: this should be handled by spell class | ||||
| 	 | ||||
| 			//for single target spells select stacks covering destination tile | ||||
| 			const bool rangeCovers = ti.massive || s->coversPos(ctx.destination); | ||||
| 			//handle smart targeting | ||||
| 			const bool positivenessFlag = !ti.smart || owner->isNeutral() || positiveToAlly || negativeToEnemy; | ||||
| 			 | ||||
| 			return rangeCovers && positivenessFlag && validTarget;		 | ||||
| 		}; | ||||
| 		 | ||||
| 		TStacks stacks = ctx.cb->battleGetStacksIf(predicate); | ||||
| 		 | ||||
| 		if(ti.massive) | ||||
| 		{ | ||||
| 			//for massive spells add all targets | ||||
| 			for (auto stack : stacks) | ||||
| 				attackedCres.insert(stack); | ||||
|  | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			//for single target spells we must select one target. Alive stack is preferred (issue #1763) | ||||
| 			for(auto stack : stacks) | ||||
| 			{ | ||||
| 				if(stack->alive()) | ||||
| 				{ | ||||
| 					attackedCres.insert(stack); | ||||
| 					break; | ||||
| 				}				 | ||||
| 			}	 | ||||
| 			 | ||||
| 			if(attackedCres.empty() && !stacks.empty()) | ||||
| 			{ | ||||
| 				attackedCres.insert(stacks.front()); | ||||
| 			}						 | ||||
| 		} | ||||
| 	} | ||||
| 	else //custom range from attackedHexes | ||||
| 	{ | ||||
| 		for(BattleHex hex : attackedHexes) | ||||
| 		{ | ||||
| 			if(const CStack * st = ctx.cb->battleGetStackByPos(hex, ti.onlyAlive)) | ||||
| 				attackedCres.insert(st); | ||||
| 		} | ||||
| 	}	 | ||||
| 	 | ||||
| 	return attackedCres; | ||||
| } | ||||
|  | ||||
|  | ||||
| ESpellCastProblem::ESpellCastProblem DefaultSpellMechanics::isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const | ||||
| { | ||||
| 	//by default use general algorithm | ||||
| 	return owner->isImmuneBy(obj); | ||||
| } | ||||
|   | ||||
| @@ -10,4 +10,44 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
|  | ||||
| #include "ISpellMechanics.h" | ||||
|  | ||||
| class BattleSpellCast; | ||||
| class StacksInjured; | ||||
|  | ||||
| struct SpellCastContext | ||||
| { | ||||
| 	SpellCastContext(std::vector<const CStack*> & attackedCres, BattleSpellCast & sc, StacksInjured & si): | ||||
| 		attackedCres(attackedCres), sc(sc), si(si){};  | ||||
| 	std::vector<const CStack*> & attackedCres; | ||||
| 	BattleSpellCast & sc; | ||||
| 	StacksInjured & si; | ||||
| }; | ||||
|  | ||||
| class DefaultSpellMechanics: public ISpellMechanics | ||||
| { | ||||
| public: | ||||
| 	DefaultSpellMechanics(CSpell * s): ISpellMechanics(s){}; | ||||
| 	 | ||||
| 	std::vector<BattleHex> rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool * outDroppedHexes = nullptr) const override; | ||||
| 	std::set<const CStack *> getAffectedStacks(SpellTargetingContext & ctx) const override; | ||||
| 	 | ||||
| 	ESpellCastProblem::ESpellCastProblem isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const override; | ||||
| 	 | ||||
| 	bool adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override final;  | ||||
| 	void battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const override; | ||||
| 	 | ||||
| 	void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const override; | ||||
| protected: | ||||
| 	 | ||||
| 	virtual void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const; | ||||
| 	 | ||||
| 	virtual int calculateDuration(const CGHeroInstance * caster, int usedSpellPower) const; | ||||
| 	 | ||||
| 	///calculate healed HP for all spells casted by hero | ||||
| 	ui32 calculateHealedHP(const CGHeroInstance* caster, const CStack* stack, const CStack* sacrificedStack) const; | ||||
| 	 | ||||
| 	///actual adventure cast implementation | ||||
| 	virtual bool applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const; | ||||
| }; | ||||
|   | ||||
| @@ -11,3 +11,86 @@ | ||||
| #include "StdInc.h" | ||||
|  | ||||
| #include "CreatureSpellMechanics.h" | ||||
|  | ||||
| #include "../NetPacks.h" | ||||
| #include "../BattleState.h" | ||||
|  | ||||
| ///AcidBreathDamageMechanics | ||||
| void AcidBreathDamageMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const | ||||
| { | ||||
| 	//calculating dmg to display | ||||
| 	ctx.sc.dmgToDisplay = parameters.usedSpellPower; | ||||
| 	 | ||||
| 	for(auto & attackedCre : ctx.attackedCres) //no immunities | ||||
| 	{ | ||||
| 		BattleStackAttacked bsa; | ||||
| 		bsa.flags |= BattleStackAttacked::SPELL_EFFECT; | ||||
| 		bsa.spellID = owner->id; | ||||
| 		bsa.damageAmount = parameters.usedSpellPower; //damage times the number of attackers | ||||
| 		bsa.stackAttacked = (attackedCre)->ID; | ||||
| 		bsa.attackerID = -1; | ||||
| 		(attackedCre)->prepareAttacked(bsa, env->getRandomGenerator()); | ||||
| 		ctx.si.stacks.push_back(bsa); | ||||
| 	}	 | ||||
| } | ||||
|  | ||||
| ///DeathStareMechanics | ||||
| void DeathStareMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const | ||||
| { | ||||
| 	//calculating dmg to display | ||||
| 	ctx.sc.dmgToDisplay = parameters.usedSpellPower; | ||||
| 	if(!ctx.attackedCres.empty()) | ||||
| 		vstd::amin(ctx.sc.dmgToDisplay, (*ctx.attackedCres.begin())->count); //stack is already reduced after attack	 | ||||
| 	 | ||||
| 	for(auto & attackedCre : ctx.attackedCres) | ||||
| 	{ | ||||
| 		BattleStackAttacked bsa; | ||||
| 		bsa.flags |= BattleStackAttacked::SPELL_EFFECT; | ||||
| 		bsa.spellID = owner->id; | ||||
| 		bsa.damageAmount = parameters.usedSpellPower * (attackedCre)->valOfBonuses(Bonus::STACK_HEALTH); | ||||
| 		bsa.stackAttacked = (attackedCre)->ID; | ||||
| 		bsa.attackerID = -1; | ||||
| 		(attackedCre)->prepareAttacked(bsa, env->getRandomGenerator()); | ||||
| 		ctx.si.stacks.push_back(bsa); | ||||
| 	}	 | ||||
| } | ||||
|  | ||||
|  | ||||
| ///DispellHelpfulMechanics | ||||
| void DispellHelpfulMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const | ||||
| { | ||||
| 	DefaultSpellMechanics::applyBattle(battle, packet); | ||||
| 	 | ||||
| 	for(auto stackID : packet->affectedCres) | ||||
| 	{ | ||||
| 		if(vstd::contains(packet->resisted, stackID)) | ||||
| 			continue; | ||||
|  | ||||
| 		CStack *s = battle->getStack(stackID); | ||||
| 		s->popBonuses([&](const Bonus *b) -> bool | ||||
| 		{ | ||||
| 			return Selector::positiveSpellEffects(b); | ||||
| 		}); | ||||
| 	}	 | ||||
| } | ||||
|  | ||||
| ESpellCastProblem::ESpellCastProblem DispellHelpfulMechanics::isImmuneByStack(const CGHeroInstance * caster,  const CStack * obj) const | ||||
| { | ||||
| 	TBonusListPtr spellBon = obj->getSpellBonuses(); | ||||
| 	bool hasPositiveSpell = false; | ||||
| 	for(const Bonus * b : *spellBon) | ||||
| 	{ | ||||
| 		if(SpellID(b->sid).toSpell()->isPositive()) | ||||
| 		{ | ||||
| 			hasPositiveSpell = true; | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
| 	if(!hasPositiveSpell) | ||||
| 	{ | ||||
| 		return ESpellCastProblem::NO_SPELLS_TO_DISPEL; | ||||
| 	} | ||||
| 	 | ||||
| 	//use default algorithm only if there is no mechanics-related problem		 | ||||
| 	return DefaultSpellMechanics::isImmuneByStack(caster,obj);	 | ||||
| } | ||||
|   | ||||
| @@ -11,3 +11,29 @@ | ||||
|  #pragma once | ||||
|   | ||||
|  #include "CDefaultSpellMechanics.h" | ||||
|  | ||||
| class AcidBreathDamageMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	AcidBreathDamageMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| protected: | ||||
| 	void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override;		 | ||||
| }; | ||||
|  | ||||
| class DeathStareMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	DeathStareMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| protected: | ||||
| 	void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override;		 | ||||
| }; | ||||
|  | ||||
| class DispellHelpfulMechanics: public DefaultSpellMechanics | ||||
| { | ||||
| public: | ||||
| 	DispellHelpfulMechanics(CSpell * s): DefaultSpellMechanics(s){}; | ||||
| 	 | ||||
| 	void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const override; | ||||
| 	 | ||||
| 	ESpellCastProblem::ESpellCastProblem isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const override;	 | ||||
| }; | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * SpellMechanics.h, part of VCMI engine | ||||
|  * ISpellMechanics.h, part of VCMI engine | ||||
|  * | ||||
|  * Authors: listed in file AUTHORS in main folder | ||||
|  * | ||||
|   | ||||
		Reference in New Issue
	
	Block a user