diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 9fc2ead37..51d1fdeb2 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -457,19 +457,15 @@ void CBattleAI::attemptCastingSpell() case OFFENSIVE_SPELL: { int damageDealt = 0, damageReceived = 0; - - auto stacksSuffering = cb->getAffectedCreatures(ps.spell, skillLevel, playerID, ps.dest); - vstd::erase_if(stacksSuffering, [&](const CStack * s) -> bool - { - return ESpellCastProblem::OK != cb->battleStackIsImmune(hero, ps.spell, ECastingMode::HERO_CASTING, s); - }); + + auto stacksSuffering = ps.spell->getAffectedStacks(cb.get(), ECastingMode::HERO_CASTING, playerID, skillLevel, ps.dest, hero); if(stacksSuffering.empty()) return -1; for(auto stack : stacksSuffering) { - const int dmg = cb->calculateSpellDmg(ps.spell, hero, stack, skillLevel, spellPower); + const int dmg = ps.spell->calculateDamage(hero, stack, skillLevel, spellPower); if(stack->owner == playerID) damageReceived += dmg; else diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 38d59aad2..f2e66ec22 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -876,17 +876,22 @@ void CPlayerInterface::battleStacksAttacked(const std::vector arg; for(auto & elem : bsa) { const CStack *defender = cb->battleGetStackByID(elem.stackAttacked, false); const CStack *attacker = cb->battleGetStackByID(elem.attackerID, false); - if(elem.isEffect() && elem.effect != 12) //and not armageddon + if(elem.isEffect()) { if (defender && !elem.isSecondary()) battleInt->displayEffect(elem.effect, defender->position); } + if(elem.isSpell()) + { + if (defender) + battleInt->displaySpellEffect(elem.spellID, defender->position); + } //FIXME: why action is deleted during enchanter cast? bool remoteAttack = false; @@ -897,11 +902,6 @@ void CPlayerInterface::battleStacksAttacked(const std::vectorisEffect() && bsa.begin()->effect == 12) //for armageddon - I hope this condition is enough - { - battleInt->displayEffect(bsa.begin()->effect, -1); - } - battleInt->stacksAreAttacked(arg); } void CPlayerInterface::battleAttack(const BattleAttack *ba) @@ -973,6 +973,15 @@ void CPlayerInterface::battleAttack(const BattleAttack *ba) const CStack * attacked = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked); battleInt->stackAttacking( attacker, ba->counter() ? curAction->destinationTile + shift : curAction->additionalInfo, attacked, false); } + + //battleInt->waitForAnims(); //FIXME: freeze + + if(ba->spellLike()) + { + //display hit animation + SpellID spellID = ba->spellID; + battleInt->displaySpellHit(spellID,curAction->destinationTile); + } } void CPlayerInterface::battleObstaclePlaced(const CObstacleInstance &obstacle) { diff --git a/client/battle/CBattleAnimations.cpp b/client/battle/CBattleAnimations.cpp index 3845ba43d..99dc88a81 100644 --- a/client/battle/CBattleAnimations.cpp +++ b/client/battle/CBattleAnimations.cpp @@ -20,6 +20,7 @@ #include "../../lib/BattleState.h" #include "../../lib/CTownHandler.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/CSpellHandler.h" /* * CBattleAnimations.cpp, part of VCMI engine @@ -865,83 +866,81 @@ void CShootingAnimation::endAnim() delete this; } -CSpellEffectAnimation::CSpellEffectAnimation(CBattleInterface * _owner, ui32 _effect, BattleHex _destTile, int _dx, int _dy, bool _Vflip, bool _areaEffect) -:CBattleAnimation(_owner), effect(_effect), destTile(_destTile), customAnim(""), x(0), y(0), dx(_dx), dy(_dy), Vflip(_Vflip) , areaEffect(_areaEffect) +CSpellEffectAnimation::CSpellEffectAnimation(CBattleInterface * _owner, ui32 _effect, BattleHex _destTile, int _dx, int _dy, bool _Vflip, bool _alignToBottom) + :CBattleAnimation(_owner), effect(_effect), destTile(_destTile), customAnim(""), x(-1), y(-1), dx(_dx), dy(_dy), Vflip(_Vflip), alignToBottom(_alignToBottom) { logAnim->debugStream() << "Created spell anim for effect #" << effect; } -CSpellEffectAnimation::CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx, int _dy, bool _Vflip, bool _areaEffect) -:CBattleAnimation(_owner), effect(-1), destTile(0), customAnim(_customAnim), x(_x), y(_y), dx(_dx), dy(_dy), Vflip(_Vflip), areaEffect(_areaEffect) +CSpellEffectAnimation::CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx, int _dy, bool _Vflip, bool _alignToBottom) + :CBattleAnimation(_owner), effect(-1), destTile(BattleHex::INVALID), customAnim(_customAnim), x(_x), y(_y), dx(_dx), dy(_dy), Vflip(_Vflip), alignToBottom(_alignToBottom) { logAnim->debugStream() << "Created spell anim for " << customAnim; } +CSpellEffectAnimation::CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip, bool _alignToBottom) + :CBattleAnimation(_owner), effect(-1), destTile(_destTile), customAnim(_customAnim), x(-1), y(-1), dx(0), dy(0), Vflip(_Vflip), alignToBottom(_alignToBottom) +{ + logAnim->debugStream() << "Created spell anim for " << customAnim; +} + + bool CSpellEffectAnimation::init() { if(!isEarliest(true)) return false; - - if(effect == 12) //armageddon + + if(customAnim.empty() && effect != ui32(-1) && !graphics->battleACToDef[effect].empty()) + { + customAnim = graphics->battleACToDef[effect][0]; + } + + if(customAnim.empty()) { - if(effect == -1 || graphics->battleACToDef[effect].size() != 0) + endAnim(); + return false; + } + + const bool areaEffect = (!destTile.isValid() && x == -1 && y == -1); + + if(areaEffect) //f.e. armageddon + { + CDefHandler * anim = CDefHandler::giveDef(customAnim); + + for(int i=0; i * anim->width < owner->pos.w ; ++i) { - CDefHandler * anim; - if(customAnim.size()) - anim = CDefHandler::giveDef(customAnim); - else - anim = CDefHandler::giveDef(graphics->battleACToDef[effect][0]); - - if (Vflip) + for(int j=0; j * anim->height < owner->pos.h ; ++j) { - for (auto & elem : anim->ourImages) + BattleEffect be; + be.effectID = ID; + be.anim = CDefHandler::giveDef(customAnim); + if (Vflip) { - CSDL_Ext::VflipSurf(elem.bitmap); - } - } - - for(int i=0; i * anim->width < owner->pos.w ; ++i) - { - for(int j=0; j * anim->height < owner->pos.h ; ++j) - { - BattleEffect be; - be.effectID = ID; - be.anim = CDefHandler::giveDef(graphics->battleACToDef[effect][0]); - if (Vflip) + for (auto & elem : be.anim->ourImages) { - for (auto & elem : be.anim->ourImages) - { - CSDL_Ext::VflipSurf(elem.bitmap); - } + CSDL_Ext::VflipSurf(elem.bitmap); } - be.currentFrame = 0; - be.maxFrame = be.anim->ourImages.size(); - be.x = i * anim->width + owner->pos.x; - be.y = j * anim->height + owner->pos.y; - be.position = BattleHex::INVALID; - - owner->battleEffects.push_back(be); } + be.currentFrame = 0; + be.maxFrame = be.anim->ourImages.size(); + be.x = i * anim->width + owner->pos.x; + be.y = j * anim->height + owner->pos.y; + be.position = BattleHex::INVALID; + + owner->battleEffects.push_back(be); } } - else //there is nothing to play - { - endAnim(); - return false; - } + + delete anim; } else // Effects targeted at a specific creature/hex. { - if(effect == -1 || graphics->battleACToDef[effect].size() != 0) - { + const CStack* destStack = owner->getCurrentPlayerInterface()->cb->battleGetStackByPos(destTile, false); Rect &tilePos = owner->bfield[destTile]->pos; BattleEffect be; be.effectID = ID; - if(customAnim.size()) - be.anim = CDefHandler::giveDef(customAnim); - else - be.anim = CDefHandler::giveDef(graphics->battleACToDef[effect][0]); + be.anim = CDefHandler::giveDef(customAnim); if (Vflip) { @@ -953,28 +952,31 @@ bool CSpellEffectAnimation::init() be.currentFrame = 0; be.maxFrame = be.anim->ourImages.size(); - if(effect == 1) - be.maxFrame = 3; + + //todo: lightning anim frame count override + +// if(effect == 1) +// be.maxFrame = 3; - switch (effect) + if(x == -1) + { + be.x = tilePos.x + tilePos.w/2 - be.anim->width/2; + } + else { - case ui32(-1): be.x = x; + } + + if(y == -1) + { + if(alignToBottom) + be.y = tilePos.y + tilePos.h - be.anim->height; + else + be.y = tilePos.y - be.anim->height/2; + } + else + { be.y = y; - break; - case 0: // Prayer and Lightning Bolt. - case 1: - case 19: // Slow - // Position effect with it's bottom center touching the bottom center of affected tile(s). - be.x = tilePos.x + tilePos.w/2 - be.anim->width/2; - be.y = tilePos.y + tilePos.h - be.anim->height; - break; - - default: - // Position effect with it's center touching the top center of affected tile(s). - be.x = tilePos.x + tilePos.w/2 - be.anim->width/2; - be.y = tilePos.y - be.anim->height/2; - break; } // Correction for 2-hex creatures. @@ -982,18 +984,10 @@ bool CSpellEffectAnimation::init() be.x += (destStack->attackerOwned ? -1 : 1)*tilePos.w/2; //Indicate if effect should be drawn on top of everything or just on top of the hex - if(areaEffect) - be.position = BattleHex::INVALID; - else - be.position = destTile; + be.position = destTile; owner->battleEffects.push_back(be); - } - else //there is nothing to play - { - endAnim(); - return false; - } + } //battleEffects return true; diff --git a/client/battle/CBattleAnimations.h b/client/battle/CBattleAnimations.h index 5ca8d6b50..5cbed2a7e 100644 --- a/client/battle/CBattleAnimations.h +++ b/client/battle/CBattleAnimations.h @@ -223,13 +223,14 @@ private: std::string customAnim; int x, y, dx, dy; bool Vflip; - bool areaEffect; + bool alignToBottom; public: bool init(); void nextFrame(); void endAnim(); - CSpellEffectAnimation(CBattleInterface * _owner, ui32 _effect, BattleHex _destTile, int _dx = 0, int _dy = 0, bool _Vflip = false, bool _areaEffect = true); - CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx = 0, int _dy = 0, bool _Vflip = false, bool _areaEffect = true); + CSpellEffectAnimation(CBattleInterface * _owner, ui32 _effect, BattleHex _destTile, int _dx = 0, int _dy = 0, bool _Vflip = false, bool _alignToBottom = false); + CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx = 0, int _dy = 0, bool _Vflip = false, bool _alignToBottom = false); + CSpellEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip = false, bool _alignToBottom = false); virtual ~CSpellEffectAnimation(){}; }; diff --git a/client/battle/CBattleInterface.cpp b/client/battle/CBattleInterface.cpp index c5f0041ce..9aa6f632f 100644 --- a/client/battle/CBattleInterface.cpp +++ b/client/battle/CBattleInterface.cpp @@ -1226,57 +1226,54 @@ void CBattleInterface::displayBattleFinished() void CBattleInterface::spellCast( const BattleSpellCast * sc ) { - const CSpell &spell = *CGI->spellh->objects[sc->id]; - - std::vector< std::string > anims; //for magic arrow and ice bolt + const SpellID spellID(sc->id); + const CSpell &spell = * spellID.toSpell(); const std::string& castSoundPath = spell.getCastSound(); if(!castSoundPath.empty()) CCS->soundh->playSound(castSoundPath); - switch(sc->id) + std::string casterCreatureName = ""; + Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos; //hero position by default { - case SpellID::MAGIC_ARROW: + const auto casterStackID = sc->casterStack; + + if(casterStackID > 0) { - //initialization of anims - anims.push_back("C20SPX0.DEF"); anims.push_back("C20SPX1.DEF"); anims.push_back("C20SPX2.DEF"); anims.push_back("C20SPX3.DEF"); anims.push_back("C20SPX4.DEF"); + const CStack * casterStack = curInt->cb->battleGetStackByID(casterStackID); + if(casterStack != nullptr) + { + casterCreatureName = casterStack->type->namePl; + + srccoord = CClickableHex::getXYUnitAnim(casterStack->position, casterStack, this); + srccoord.x += 250; + srccoord.y += 240; + } } - case SpellID::ICE_BOLT: + } + + //TODO: play custom cast animation + { + + } + + //playing projectile animation + if(sc->tile.isValid()) + { + Point destcoord = CClickableHex::getXYUnitAnim(sc->tile, curInt->cb->battleGetStackByPos(sc->tile), this); //position attacked by projectile + destcoord.x += 250; destcoord.y += 240; + + //animation angle + double angle = atan2(static_cast(destcoord.x - srccoord.x), static_cast(destcoord.y - srccoord.y)); + bool Vflip = (angle < 0); + if(Vflip) + angle = -angle; + + std::string animToDisplay = spell.animationInfo.selectProjectile(angle); + + if(!animToDisplay.empty()) { - if(anims.size() == 0) //initialization of anims - { - anims.push_back("C08SPW0.DEF"); anims.push_back("C08SPW1.DEF"); anims.push_back("C08SPW2.DEF"); anims.push_back("C08SPW3.DEF"); anims.push_back("C08SPW4.DEF"); - } - } //end of ice bolt only part - { //common ice bolt and magic arrow part - //initial variables - std::string animToDisplay; - Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos; - Point destcoord = CClickableHex::getXYUnitAnim(sc->tile, curInt->cb->battleGetStackByPos(sc->tile), this); //position attacked by arrow - destcoord.x += 250; destcoord.y += 240; - - //animation angle - double angle = atan2(static_cast(destcoord.x - srccoord.x), static_cast(destcoord.y - srccoord.y)); - bool Vflip = false; - if (angle < 0) - { - Vflip = true; - angle = -angle; - } - - //choosing animation by angle - if(angle > 1.50) - animToDisplay = anims[0]; - else if(angle > 1.20) - animToDisplay = anims[1]; - else if(angle > 0.90) - animToDisplay = anims[2]; - else if(angle > 0.60) - animToDisplay = anims[3]; - else - animToDisplay = anims[4]; - //displaying animation CDefEssential * animDef = CDefHandler::giveDefEss(animToDisplay); double diffX = (destcoord.x - srccoord.x)*(destcoord.x - srccoord.x); @@ -1288,31 +1285,26 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc ) int dy = (destcoord.y - srccoord.y - animDef->ourImages[0].bitmap->h)/steps; delete animDef; - addNewAnim(new CSpellEffectAnimation(this, animToDisplay, srccoord.x, srccoord.y, dx, dy, Vflip)); + addNewAnim(new CSpellEffectAnimation(this, animToDisplay, srccoord.x, srccoord.y, dx, dy, Vflip)); + } + } + waitForAnims(); + + displaySpellHit(spellID, sc->tile); + + //queuing affect /resist animation + for (auto & elem : sc->affectedCres) + { + BattleHex position = curInt->cb->battleGetStackByID(elem, false)->position; + + if(vstd::contains(sc->resisted,elem)) + displayEffect(78, position); + else + displaySpellEffect(spellID, position); + } - break; //for 15 and 16 cases - } - case SpellID::LIGHTNING_BOLT: - case SpellID::TITANS_LIGHTNING_BOLT: - case SpellID::THUNDERBOLT: - case SpellID::CHAIN_LIGHTNING: //TODO: zigzag effect - for (auto & elem : sc->affectedCres) //in case we have multiple targets - { - displayEffect(1, curInt->cb->battleGetStackByID(elem, false)->position); - displayEffect(spell.mainEffectAnim, curInt->cb->battleGetStackByID(elem, false)->position); - } - break; - case SpellID::DISPEL: - case SpellID::CURE: - case SpellID::RESURRECTION: - case SpellID::ANIMATE_DEAD: - case SpellID::DISPEL_HELPFUL_SPELLS: - case SpellID::SACRIFICE: //TODO: animation upon killed stack - for(auto & elem : sc->affectedCres) - { - displayEffect(spell.mainEffectAnim, curInt->cb->battleGetStackByID(elem, false)->position); - } - break; + switch(sc->id) + { case SpellID::SUMMON_FIRE_ELEMENTAL: case SpellID::SUMMON_EARTH_ELEMENTAL: case SpellID::SUMMON_WATER_ELEMENTAL: @@ -1323,19 +1315,6 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc ) break; } //switch(sc->id) - if (spell.isDamageSpell() && sc->affectedCres.empty()) //for example Inferno that causes no BattleStackAttacked - { - if(sc->tile.isValid() && graphics->battleACToDef.count(spell.mainEffectAnim)) //eg. when casting Lind Mine or Fire Wall - displayEffect (spell.mainEffectAnim, sc->tile); - } - - //support for resistance - for(auto & elem : sc->resisted) - { - int tile = curInt->cb->battleGetStackByID(elem)->position; - displayEffect(78, tile); - } - //displaying message in console bool customSpell = false; if(sc->affectedCres.size() == 1) @@ -1393,7 +1372,7 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc ) } //The %s shrivel with age, and lose %d hit points." TBonusListPtr bl = curInt->cb->battleGetStackByID(*sc->affectedCres.begin(), false)->getBonuses(Selector::type(Bonus::STACK_HEALTH)); - bl->remove_if(Selector::source(Bonus::SPELL_EFFECT, 75)); + bl->remove_if(Selector::source(Bonus::SPELL_EFFECT, SpellID::AGE)); boost::algorithm::replace_first(text, "%d", boost::lexical_cast(bl->totalValue()/2)); } break; @@ -1427,15 +1406,15 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc ) text = CGI->generaltexth->allTexts[118]; //One %s dies under the terrible gaze of the %s. boost::algorithm::replace_first(text, "%s", curInt->cb->battleGetStackByID(*sc->affectedCres.begin())->type->nameSing); } - boost::algorithm::replace_first(text, "%s", CGI->creh->creatures[sc->attackerType]->namePl); //casting stack + boost::algorithm::replace_first(text, "%s", casterCreatureName); //casting stack } else text = ""; break; default: text = CGI->generaltexth->allTexts[565]; //The %s casts %s - if(auto castingCreature = vstd::atOrDefault(CGI->creh->creatures, sc->attackerType, nullptr)) - boost::algorithm::replace_first(text, "%s", castingCreature->namePl); //casting stack + if(casterCreatureName != "") + boost::algorithm::replace_first(text, "%s", casterCreatureName); //casting stack else boost::algorithm::replace_first(text, "%s", "@Unknown caster@"); //should not happen } @@ -1465,9 +1444,9 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc ) { boost::algorithm::replace_first(text, "%s", curInt->cb->battleGetHeroInfo(sc->side).name); } - if(auto castingCreature = vstd::atOrDefault(CGI->creh->creatures, sc->attackerType, nullptr)) + if(casterCreatureName != "") { - boost::algorithm::replace_first(text, "%s", castingCreature->namePl); //creature caster + boost::algorithm::replace_first(text, "%s", casterCreatureName); //creature caster } else { @@ -1486,7 +1465,7 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc ) } waitForAnims(); //mana absorption - if (sc->manaGained) + if(sc->manaGained > 0) { Point leftHero = Point(15, 30) + pos; Point rightHero = Point(755, 30) + pos; @@ -1497,16 +1476,7 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc ) void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) { - int effID = sse.effect.back().sid; - if(effID != -1) //can be -1 for defensive stance effect - { - for(auto & elem : sse.stacks) - { - bool areaEffect(CGI->spellh->objects[effID]->getTargetType() == CSpell::ETargetType::NO_TARGET); - displayEffect(CGI->spellh->objects[effID]->mainEffectAnim, curInt->cb->battleGetStackByID(elem)->position, areaEffect); - } - } - else if (sse.stacks.size() == 1 && sse.effect.size() == 2) + if (sse.effect.back().sid == -1 && sse.stacks.size() == 1 && sse.effect.size() == 2) { const Bonus & bns = sse.effect.front(); if (bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL) @@ -1552,8 +1522,12 @@ void CBattleInterface::castThisSpell(int spellID) const CSpell::TargetInfo ti = sp->getTargetInfo(castingHero->getSpellSchoolLevel(sp)); - if(ti.massive) + if(ti.massive || ti.type == CSpell::NO_TARGET) spellSelMode = NO_LOCATION; + else if(ti.type == CSpell::LOCATION && ti.clearAffected) + { + spellSelMode = FREE_LOCATION; + } else if(ti.type == CSpell::CREATURE) { if(ti.smart) @@ -1565,11 +1539,6 @@ void CBattleInterface::castThisSpell(int spellID) { spellSelMode = OBSTACLE; } - //todo: move to JSON config - if(spellID == SpellID::FIRE_WALL || spellID == SpellID::FORCE_FIELD) - { - spellSelMode = FREE_LOCATION; - } if (spellSelMode == NO_LOCATION) //user does not have to select location { @@ -1587,9 +1556,37 @@ void CBattleInterface::castThisSpell(int spellID) void CBattleInterface::displayEffect(ui32 effect, int destTile, bool areaEffect) { - addNewAnim(new CSpellEffectAnimation(this, effect, destTile, 0, 0, false, areaEffect)); + //todo: recheck areaEffect usage + addNewAnim(new CSpellEffectAnimation(this, effect, destTile, 0, 0, false)); } +void CBattleInterface::displaySpellEffect(SpellID spellID, BattleHex destinationTile, bool areaEffect) +{ + const CSpell * spell = spellID.toSpell(); + + if(spell == nullptr) + return; + + for(const CSpell::TAnimation & animation : spell->animationInfo.affect) + { + addNewAnim(new CSpellEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM)); + } +} + +void CBattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTile, bool areaEffect) +{ + const CSpell * spell = spellID.toSpell(); + + if(spell == nullptr) + return; + + for(const CSpell::TAnimation & animation : spell->animationInfo.hit) + { + addNewAnim(new CSpellEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM)); + } +} + + void CBattleInterface::battleTriggerEffect(const BattleTriggerEffect & bte) { const CStack * stack = curInt->cb->battleGetStackByID(bte.stackID); @@ -1792,7 +1789,6 @@ void CBattleInterface::getPossibleActionsForStack(const CStack * stack) void CBattleInterface::printConsoleAttacked( const CStack * defender, int dmg, int killed, const CStack * attacker, bool multiple ) { boost::format txt; - int end = 0; if (attacker) //ignore if stacks were killed by spell { txt = boost::format (CGI->generaltexth->allTexts[attacker->count > 1 ? 377 : 376]) % diff --git a/client/battle/CBattleInterface.h b/client/battle/CBattleInterface.h index cbb11ad94..7706417cc 100644 --- a/client/battle/CBattleInterface.h +++ b/client/battle/CBattleInterface.h @@ -318,7 +318,10 @@ public: void spellCast(const BattleSpellCast * sc); //called when a hero casts a spell void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks void castThisSpell(int spellID); //called when player has chosen a spell from spellbook - void displayEffect(ui32 effect, int destTile, bool areaEffect = true); //displays effect of a spell on the battlefield; affected: true - attacker. false - defender + void displayEffect(ui32 effect, int destTile, bool areaEffect = true); //displays custom effect on the battlefield + void displaySpellEffect(SpellID spellID, BattleHex destinationTile, bool areaEffect = true); //displays spell`s affected animation + void displaySpellHit(SpellID spellID, BattleHex destinationTile, bool areaEffect = true); //displays spell`s affected animation + void battleTriggerEffect(const BattleTriggerEffect & bte); void setBattleCursor(const int myNumber); //really complex and messy, sets attackingHex void endAction(const BattleAction* action); diff --git a/client/battle/CBattleInterfaceClasses.cpp b/client/battle/CBattleInterfaceClasses.cpp index f08e79b69..9b6f5cd81 100644 --- a/client/battle/CBattleInterfaceClasses.cpp +++ b/client/battle/CBattleInterfaceClasses.cpp @@ -537,7 +537,7 @@ Point CClickableHex::getXYUnitAnim(BattleHex hexNum, const CStack * stack, CBatt } } //returning - return ret +CPlayerInterface::battleInt->pos; + return ret + CPlayerInterface::battleInt->pos; } void CClickableHex::hover(bool on) diff --git a/config/battles_graphics.json b/config/battles_graphics.json index 7cb41fa8b..f07e72ec6 100644 --- a/config/battles_graphics.json +++ b/config/battles_graphics.json @@ -30,80 +30,80 @@ // WoG_Ac_format_to_def_names_mapping "ac_mapping": [ - { "id": 0, "defnames": [ "C10SPW.DEF" ] }, - { "id": 1, "defnames": [ "C03SPA0.DEF" ] }, - { "id": 2, "defnames": [ "C01SPA0.DEF" ] }, - { "id": 3, "defnames": [ "C02SPA0.DEF" ] }, - { "id": 4, "defnames": [ "SP12_.DEF" ] }, - { "id": 5, "defnames": [ "C02SPE0.DEF" ] }, - { "id": 6, "defnames": [ "C02SPF0.DEF" ] }, - { "id": 7, "defnames": [ "C04SPA0.DEF" ] }, - { "id": 8, "defnames": [ "C04SPE0.DEF" ] }, - { "id": 9, "defnames": [ "C04SPF0.DEF" ] }, - { "id": 10, "defnames": [ "C05SPE0.DEF" ] }, - { "id": 11, "defnames": [ "C05SPF0.DEF" ] }, - { "id": 12, "defnames": [ "C06SPF0.DEF" ] }, - { "id": 13, "defnames": [ "C07SPA0.DEF" ] }, - { "id": 14, "defnames": [ "C07SPA1.DEF" ] }, + { "id": 0, "defnames": [ "C10SPW.DEF" ] },//merged + { "id": 1, "defnames": [ "C03SPA0.DEF" ] },//merged + { "id": 2, "defnames": [ "C01SPA0.DEF" ] },//merged + { "id": 3, "defnames": [ "C02SPA0.DEF" ] },//merged + { "id": 4, "defnames": [ "SP12_.DEF" ] },//merged (?) + { "id": 5, "defnames": [ "C02SPE0.DEF" ] },//merged + { "id": 6, "defnames": [ "C02SPF0.DEF" ] },//merged + { "id": 7, "defnames": [ "C04SPA0.DEF" ] },//merged + { "id": 8, "defnames": [ "C04SPE0.DEF" ] },//merged + { "id": 9, "defnames": [ "C04SPF0.DEF" ] },//merged + { "id": 10, "defnames": [ "C05SPE0.DEF" ] },//merged + { "id": 11, "defnames": [ "C05SPF0.DEF" ] },//merged + { "id": 12, "defnames": [ "C06SPF0.DEF" ] },//merged (?) + { "id": 13, "defnames": [ "C07SPA0.DEF" ] },//merged (?) + { "id": 14, "defnames": [ "C07SPA1.DEF" ] },//merged { "id": 15, "defnames": [ "C0FEAR.DEF" ] }, { "id": 16, "defnames": [ "C08SPE0.DEF" ] }, - { "id": 17, "defnames": [ "C08SPF0.DEF" ] }, - { "id": 18, "defnames": [ "C09SPA0.DEF" ] }, - { "id": 19, "defnames": [ "C09SPE0.DEF" ] }, - { "id": 20, "defnames": [ "C09SPW0.DEF" ] }, - { "id": 21, "defnames": [ "C10SPA0.DEF" ] }, - { "id": 22, "defnames": [ "C11SPE0.DEF" ] }, - { "id": 23, "defnames": [ "C11SPF0.DEF" ] }, - { "id": 24, "defnames": [ "C11SPW0.DEF" ] }, - { "id": 25, "defnames": [ "C12SPA0.DEF" ] }, - { "id": 26, "defnames": [ "C13SPA0.DEF" ] }, - { "id": 27, "defnames": [ "C13SPE0.DEF" ] }, - { "id": 28, "defnames": [ "C13SPW0.DEF" ] }, - { "id": 29, "defnames": [ "C14SPA0.DEF" ] }, - { "id": 30, "defnames": [ "C14SPE0.DEF" ] }, - { "id": 31, "defnames": [ "C15SPA0.DEF" ] }, + { "id": 17, "defnames": [ "C08SPF0.DEF" ] },//merged + { "id": 18, "defnames": [ "C09SPA0.DEF" ] },//merged + { "id": 19, "defnames": [ "C09SPE0.DEF" ] },//merged + { "id": 20, "defnames": [ "C09SPW0.DEF" ] },//merged + { "id": 21, "defnames": [ "C10SPA0.DEF" ] },//merged + { "id": 22, "defnames": [ "C11SPE0.DEF" ] },//merged + { "id": 23, "defnames": [ "C11SPF0.DEF" ] },//merged + { "id": 24, "defnames": [ "C11SPW0.DEF" ] },//merged + { "id": 25, "defnames": [ "C12SPA0.DEF" ] },//merged + { "id": 26, "defnames": [ "C13SPA0.DEF" ] },//merged + { "id": 27, "defnames": [ "C13SPE0.DEF" ] },//merged + { "id": 28, "defnames": [ "C13SPW0.DEF" ] },//merged + { "id": 29, "defnames": [ "C14SPA0.DEF" ] },//merged + { "id": 30, "defnames": [ "C14SPE0.DEF" ] },//merged + { "id": 31, "defnames": [ "C15SPA0.DEF" ] },//merged { "id": 32, "defnames": [ "C15SPE0.DEF", "C15SPE1.DEF", "C15SPE2.DEF" ] }, { "id": 33, "defnames": [ "C15SPE3.DEF", "C15SPE6.DEF", "C15SPE7.DEF", "C15SPE8.DEF", "C15SPE9.DEF", "C15SPE10.DEF", "C15SPE11.DEF" ] }, - { "id": 35, "defnames": [ "C01SPF.DEF", "C01SPF0.DEF" ] }, - { "id": 36, "defnames": [ "C01SPW.DEF", "C01SPW0.DEF" ] }, - { "id": 38, "defnames": [ "C11SPA1.DEF" ] }, - { "id": 39, "defnames": [ "C03SPW.DEF", "C03SPW0.DEF" ] }, - { "id": 40, "defnames": [ "C04SPW.DEF", "C04SPW0.DEF" ] }, - { "id": 41, "defnames": [ "C05SPW.DEF", "C05SPW0.DEF" ] }, - { "id": 42, "defnames": [ "C06SPW.DEF", "C06SPW0.DEF" ] }, + { "id": 35, "defnames": [ "C01SPF.DEF", "C01SPF0.DEF" ] },//merged + { "id": 36, "defnames": [ "C01SPW.DEF", "C01SPW0.DEF" ] },//merged + { "id": 38, "defnames": [ "C11SPA1.DEF" ] },//merged + { "id": 39, "defnames": [ "C03SPW.DEF", "C03SPW0.DEF" ] },//merged + { "id": 40, "defnames": [ "C04SPW.DEF", "C04SPW0.DEF" ] },//merged + { "id": 41, "defnames": [ "C05SPW.DEF", "C05SPW0.DEF" ] },//merged + { "id": 42, "defnames": [ "C06SPW.DEF", "C06SPW0.DEF" ] },//merged { "id": 43, "defnames": [ "C07SPF0.DEF", "C07SPF1.DEF", "C07SPF2.DEF", "C07SPF6.DEF", "C07SPF7.DEF", "C07SPF8.DEF" ] }, { "id": 44, "defnames": [ "C07SPF0.DEF", "C07SPF4.DEF", "C07SPF5.DEF", "C07SPF9.DEF", "C07SPF10.DEF", "C07SPF11.DEF" ] }, - { "id": 45, "defnames": [ "C07SPW.DEF", "C07SPW0.DEF" ] }, - { "id": 46, "defnames": [ "C08SPW5.DEF" ] }, + { "id": 45, "defnames": [ "C07SPW.DEF", "C07SPW0.DEF" ] },//merged + { "id": 46, "defnames": [ "C08SPW5.DEF" ] },//merged (?) { "id": 47, "defnames": [ "C09SPF0.DEF" ] }, - { "id": 48, "defnames": [ "C10SPF0.DEF" ] }, + { "id": 48, "defnames": [ "C10SPF0.DEF" ] },//merged { "id": 49, "defnames": [ "C11SPA1.DEF" ] }, { "id": 50, "defnames": [ "C12SPE0.DEF" ] }, { "id": 51, "defnames": [ "C12SPF0.DEF" ] }, { "id": 52, "defnames": [ "SP06_.DEF" ] }, - { "id": 53, "defnames": [ "C13SPF.DEF", "C13SPF0.DEF" ] }, - { "id": 54, "defnames": [ "C16SPE.DEF", "C16SPE0.DEF" ] }, + { "id": 53, "defnames": [ "C13SPF.DEF", "C13SPF0.DEF" ] }, //merged + { "id": 54, "defnames": [ "C16SPE.DEF", "C16SPE0.DEF" ] }, //merged { "id": 55, "defnames": [ "C17SPE0.DEF" ] }, - { "id": 56, "defnames": [ "C0ACID.DEF" ] }, + { "id": 56, "defnames": [ "C0ACID.DEF" ] },//merged { "id": 57, "defnames": [ "C09SPF1.DEF", "C09SPF2.DEF" ] }, { "id": 58, "defnames": [ "C17SPE2.DEF" ] }, { "id": 59, "defnames": [ "C09SPF0.DEF" ] }, { "id": 62, "defnames": [ "C07SPF60.DEF", "C07SPF61.DEF", "C07SPF62.DEF" ] }, - { "id": 64, "defnames": [ "C20SPX.DEF" ] }, - { "id": 67, "defnames": [ "SP11_.DEF" ] }, - { "id": 68, "defnames": [ "SP02_.DEF" ] }, - { "id": 69, "defnames": [ "SP05_.DEF" ] }, - { "id": 71, "defnames": [ "SP01_.DEF" ] }, - { "id": 72, "defnames": [ "SP04_.DEF" ] }, + { "id": 64, "defnames": [ "C20SPX.DEF" ] }, //merged + { "id": 67, "defnames": [ "SP11_.DEF" ] }, //merged + { "id": 68, "defnames": [ "SP02_.DEF" ] }, //merged + { "id": 69, "defnames": [ "SP05_.DEF" ] }, //merged + { "id": 71, "defnames": [ "SP01_.DEF" ] }, //merged + { "id": 72, "defnames": [ "SP04_.DEF" ] }, //merged { "id": 73, "defnames": [ "SP03_.DEF" ] }, { "id": 74, "defnames": [ "SP12_.DEF" ] }, { "id": 75, "defnames": [ "SP07_A.DEF" ] }, { "id": 76, "defnames": [ "SP07_B.DEF" ] }, { "id": 77, "defnames": [ "SP08_.DEF" ] }, { "id": 78, "defnames": [ "SP09_.DEF" ] }, - { "id": 79, "defnames": [ "C01SPE0.DEF" ] }, - { "id": 80, "defnames": [ "C07SPE0.DEF" ] }, - { "id": 81, "defnames": [ "C17SPW0.DEF" ] }, + { "id": 79, "defnames": [ "C01SPE0.DEF" ] },//merged + { "id": 80, "defnames": [ "C07SPE0.DEF" ] },//merged + { "id": 81, "defnames": [ "C17SPW0.DEF" ] },//merged { "id": 82, "defnames": [ "C09SPF3.DEF" ] }, { "id": 84, "defnames": [ "ZMGC02.DEF" ] } ] diff --git a/config/schemas/spell.json b/config/schemas/spell.json index 7d0084f38..32abd5072 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -8,6 +8,46 @@ "definitions" : { + "animationQueue":{ + "type": "array", + "items":{ + "anyOf":[ + { + //assumed verticalPosition: top + "type": "string", + "format": "defFile" + }, + { + "type": "object", + "properties":{ + "verticalPosition": {"type":"string", "enum":["top","bottom"]}, + "defName": {"type":"string", "format": "defFile"} + }, + "additionalProperties" : false + } + ] + } + }, + "animation":{ + "type": "object", + "additionalProperties" : false, + "properties":{ + "affect":{"$ref" : "#/definitions/animationQueue"}, + "hit":{"$ref" : "#/definitions/animationQueue"}, + "cast":{"$ref" : "#/definitions/animationQueue"}, + "projectile":{ + "type":"array", + "items":{ + "type": "object", + "properties":{ + "minimumAngle": {"type":"number", "minimum" : 0}, + "defName": {"type":"string", "format": "defFile"} + }, + "additionalProperties" : false + } + } + } + }, "flags" :{ "type" : "object", "additionalProperties" : { @@ -53,10 +93,27 @@ "smart":{ "type": "boolean", "description": "true: friendly/hostile based on positiveness; false: all targets" - } + }, + "clearTarget": + { + "type": "boolean", + "description": "LOCATION target only. Target hex/tile must be clear" + }, + "clearAffected": + { + "type": "boolean", + "description": "LOCATION target only. All affected hexes/tile must be clear" + } } } } + }, + + "texts":{ + "type": "object", + + + "additionalProperties" : false } }, @@ -114,18 +171,14 @@ "type": "object", "description": "Chance in % to gain for faction. NOTE: this field is merged with faction config", "additionalProperties" : { - "type": "number", - "minimum" : 0 - } + "type": "number", + "minimum" : 0 + } }, "targetType":{ "type": "string", - "enum": ["NO_TARGET","CREATURE","OBSTACLE"] - }, - "anim":{ - "type": "number", - "description": "Main effect animation (AC format), -1 - none, deprecated", - "minimum": -1 + "description": "NO_TARGET - instant cast no aiming, default; CREATURE - target is unit; OBSTACLE - target is OBSTACLE; LOCATION - target is location", + "enum": ["NO_TARGET","CREATURE","OBSTACLE","LOCATION"] }, "counters":{ "$ref" : "#/definitions/flags", @@ -182,6 +235,9 @@ "$ref" : "#/definitions/flags", "description": "flags structure of bonus names, presence of all bonuses required to be affected by, can't be negated." }, + + "animation":{"$ref": "#/definitions/animation"}, + "graphics":{ "type": "object", "additionalProperties" : false, diff --git a/config/spells/ability.json b/config/spells/ability.json index 8035495cd..5f1ba9530 100644 --- a/config/spells/ability.json +++ b/config/spells/ability.json @@ -1,7 +1,10 @@ { "stoneGaze" : { "index" : 70, - "anim" : 70, + "targetType": "NO_TARGET", + "animation":{ + //need special animation + }, "sounds": { "cast": "PARALYZE" }, @@ -33,7 +36,11 @@ }, "poison" : { "index" : 71, - "anim" : 67, + "targetType": "NO_TARGET", + + "animation":{ + "affect":["SP11_"] + }, "sounds": { "cast": "POISON" }, @@ -67,7 +74,11 @@ }, "bind" : { "index" : 72, - "anim" : 68, + "targetType": "NO_TARGET", + + "animation":{ + "affect":["SP02_"] + }, "sounds": { "cast": "BIND" }, @@ -90,7 +101,11 @@ }, "disease" : { "index" : 73, - "anim" : 69, + "targetType": "NO_TARGET", + + "animation":{ + "affect":["SP05_"] + }, "sounds": { "cast": "DISEASE" }, @@ -124,7 +139,11 @@ }, "paralyze" : { "index" : 74, - "anim" : 70, + "targetType": "NO_TARGET", + + "animation":{ + //missing + }, "sounds": { "cast": "PARALYZE" }, @@ -156,7 +175,11 @@ }, "age" : { "index" : 75, - "anim" : 71, + "targetType": "NO_TARGET", + + "animation":{ + "affect":["SP01_"] + }, "sounds": { "cast": "AGE" }, @@ -184,7 +207,11 @@ }, "deathCloud" : { "index" : 76, - "anim" : 72, + "targetType": "NO_TARGET", + + "animation":{ + "hit":["SP04_"] + }, "sounds": { "cast": "DEATHCLD" }, @@ -203,7 +230,11 @@ }, "thunderbolt" : { "index" : 77, - "anim" : 38, + "targetType": "NO_TARGET", + + "animation":{ + "affect":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"] + }, "sounds": { "cast": "LIGHTBLT" }, @@ -220,7 +251,11 @@ }, "dispelHelpful" : { "index" : 78, - "anim" : 41, + "targetType": "NO_TARGET", + + "animation":{ + "affect":["C05SPW"] + }, "sounds": { "cast": "DISPELL" }, @@ -236,7 +271,11 @@ }, "deathStare" : { "index" : 79, - "anim" : 80, + "targetType": "NO_TARGET", + + "animation":{ + "affect":["C07SPE0"] + }, "sounds": { "cast": "DEATHSTR" }, @@ -255,7 +294,11 @@ }, "acidBreath" : { "index" : 80, - "anim" : 81, + "targetType": "NO_TARGET", + + "animation":{ + //??? + }, "sounds": { "cast": "ACID" }, @@ -280,7 +323,11 @@ }, "acidBreathDamage" : { "index" : 81, - "anim" : 81, + "targetType": "NO_TARGET", + + "animation":{ + "affect":["C17SPW0"] + }, "sounds": { "cast": "ACID" }, diff --git a/config/spells/adventure.json b/config/spells/adventure.json index 2ecd0b474..efff16442 100644 --- a/config/spells/adventure.json +++ b/config/spells/adventure.json @@ -1,7 +1,8 @@ { "summonBoat" : { "index" : 0, - "anim" : -1, + + "targetType": "NO_TARGET", "sounds": { "cast": "SUMMBOAT" }, @@ -16,7 +17,8 @@ }, "scuttleBoat" : { "index" : 1, - "anim" : -1, + "targetType": "NO_TARGET", + "sounds": { "cast": "SCUTBOAT" }, @@ -31,7 +33,8 @@ }, "visions" : { "index" : 2, - "anim" : -1, + "targetType": "NO_TARGET", + "sounds": { "cast": "VISIONS" }, @@ -46,7 +49,8 @@ }, "viewEarth" : { "index" : 3, - "anim" : -1, + "targetType": "NO_TARGET", + "sounds": { "cast": "VIEW" }, @@ -61,7 +65,8 @@ }, "disguise" : { "index" : 4, - "anim" : -1, + "targetType": "NO_TARGET", + "sounds": { "cast": "DISGUISE" }, @@ -76,7 +81,8 @@ }, "viewAir" : { "index" : 5, - "anim" : -1, + "targetType": "NO_TARGET", + "sounds": { "cast": "VIEW" }, @@ -91,7 +97,8 @@ }, "fly" : { "index" : 6, - "anim" : -1, + "targetType": "NO_TARGET", + "sounds": { "cast": "FLYSPELL" }, @@ -106,7 +113,8 @@ }, "waterWalk" : { "index" : 7, - "anim" : -1, + "targetType": "NO_TARGET", + "sounds": { "cast": "WATRWALK" }, @@ -121,7 +129,8 @@ }, "dimensionDoor" : { "index" : 8, - "anim" : -1, + "targetType": "LOCATION", + "sounds": { "cast": "TELPTOUT" }, @@ -136,7 +145,8 @@ }, "townPortal" : { "index" : 9, - "anim" : -1, + "targetType": "NO_TARGET", + "sounds": { "cast": "TELPTOUT" }, diff --git a/config/spells/offensive.json b/config/spells/offensive.json index 30df68797..b73ce80f5 100644 --- a/config/spells/offensive.json +++ b/config/spells/offensive.json @@ -1,7 +1,18 @@ { "magicArrow" : { "index" : 15, - "anim" : 64, + "targetType": "CREATURE", + + "animation":{ + "projectile": [ + {"minimumAngle": 0 ,"defName":"C20SPX4"}, + {"minimumAngle": 0.60 ,"defName":"C20SPX3"}, + {"minimumAngle": 0.90 ,"defName":"C20SPX2"}, + {"minimumAngle": 1.20 ,"defName":"C20SPX1"}, + {"minimumAngle": 1.50 ,"defName":"C20SPX0"} + ], + "hit":["C20SPX"] + }, "sounds": { "cast": "MAGICBLT" }, @@ -22,7 +33,18 @@ }, "iceBolt" : { "index" : 16, - "anim" : 46, + "targetType": "CREATURE", + + "animation":{ + "projectile": [ + {"minimumAngle": 0 ,"defName":"C08SPW4"}, + {"minimumAngle": 0.60 ,"defName":"C08SPW3"}, + {"minimumAngle": 0.90 ,"defName":"C08SPW2"}, + {"minimumAngle": 1.20 ,"defName":"C08SPW1"}, + {"minimumAngle": 1.50 ,"defName":"C08SPW0"} + ], + "hit":["C08SPW5"] + }, "sounds": { "cast": "ICERAY" }, @@ -43,7 +65,11 @@ }, "lightningBolt" : { "index" : 17, - "anim" : 38, + "targetType": "CREATURE", + + "animation":{ + "affect":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"] + }, "sounds": { "cast": "LIGHTBLT" }, @@ -64,7 +90,11 @@ }, "implosion" : { "index" : 18, - "anim" : 10, + "targetType": "CREATURE", + + "animation":{ + "affect":["C05SPE0"] + }, "sounds": { "cast": "DECAY" }, @@ -86,7 +116,11 @@ }, "chainLightning" : { "index" : 19, - "anim" : 38, + "targetType": "CREATURE", + + "animation":{ + "affect":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"] + }, "sounds": { "cast": "CHAINLTE" }, @@ -104,7 +138,11 @@ }, "frostRing" : { "index" : 20, - "anim" : 45, + "targetType": "LOCATION", + + "animation":{ + "hit":["C07SPW"] //C07SPW0 ??? + }, "sounds": { "cast": "FROSTING" }, @@ -125,7 +163,11 @@ }, "fireball" : { "index" : 21, - "anim" : 53, + "targetType": "LOCATION", + + "animation":{ + "hit":["C13SPF"] //C13SPF0 ??? + }, "sounds": { "cast": "FIREBALL" }, @@ -146,7 +188,11 @@ }, "inferno" : { "index" : 22, - "anim" : 9, + "targetType": "LOCATION", + + "animation":{ + "hit":["C04SPF0"] + }, "sounds": { "cast": "FIREBLST" }, @@ -167,7 +213,11 @@ }, "meteorShower" : { "index" : 23, - "anim" : 16, + "targetType": "LOCATION", + + "animation":{ + "hit":["C08SPE0"] + }, "sounds": { "cast": "METEOR" }, @@ -189,7 +239,10 @@ "deathRipple" : { "index" : 24, "targetType" : "CREATURE", - "anim" : 8, + + "animation":{ + "affect":["C04SPE0"] + }, "sounds": { "cast": "DEATHRIP" }, @@ -215,7 +268,10 @@ "destroyUndead" : { "index" : 25, "targetType" : "CREATURE", - "anim" : 29, + + "animation":{ + "affect":["C14SPA0"] + }, "sounds": { "cast": "COLDRING" }, @@ -240,7 +296,10 @@ "armageddon" : { "index" : 26, "targetType" : "CREATURE", - "anim" : 12, + + "animation":{ + "hit":["C06SPF0"] + }, "sounds": { "cast": "ARMGEDN" }, @@ -261,7 +320,11 @@ }, "titanBolt" : { "index" : 57, - "anim" : 38, + "targetType" : "CREATURE", + + "animation":{ + "hit":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"] + }, "sounds": { "cast": "LIGHTBLT" }, @@ -277,5 +340,5 @@ "negative": true, "special": true } - }, + } } diff --git a/config/spells/other.json b/config/spells/other.json index 7313493ec..90576936d 100644 --- a/config/spells/other.json +++ b/config/spells/other.json @@ -1,7 +1,8 @@ { "quicksand" : { "index" : 10, - "anim" : -1, + "targetType" : "NO_TARGET", + "sounds": { "cast": "QUIKSAND" }, @@ -16,7 +17,8 @@ }, "landMine" : { "index" : 11, - "anim" : -1, + "targetType" : "NO_TARGET", + "sounds": { "cast": "" }, @@ -35,13 +37,17 @@ }, "forceField" : { "index" : 12, - "anim" : -1, + "targetType" : "LOCATION", + "sounds": { "cast": "FORCEFLD" }, "levels" : { "base":{ - "range" : "0" + "range" : "0", + "targetModifier":{ + "clearAffected": true + } } }, "flags" : { @@ -50,13 +56,17 @@ }, "fireWall" : { "index" : 13, - "anim" : -1, + "targetType" : "LOCATION", + "sounds": { "cast": "FIREWALL" }, "levels" : { "base":{ - "range" : "0" + "range" : "0", + "targetModifier":{ + "clearAffected": true + } } }, "flags" : { @@ -69,7 +79,8 @@ }, "earthquake" : { "index" : 14, - "anim" : -1, + "targetType" : "NO_TARGET", + "sounds": { "cast": "ERTHQUAK" }, @@ -85,7 +96,11 @@ "dispel" : { "index" : 35, - "anim" : 41, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C05SPW"] //C05SPW0 + }, "sounds": { "cast": "DISPELL" }, @@ -103,7 +118,11 @@ }, "cure" : { "index" : 37, - "anim" : 39, + "targetType" : "CREATURE", + "animation":{ + "affect":["C03SPW"]//C03SPW0 + }, + "sounds": { "cast": "CURE" }, @@ -122,7 +141,11 @@ }, "resurrection" : { "index" : 38, - "anim" : 79, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C01SPE0"] + }, "sounds": { "cast": "RESURECT" }, @@ -143,7 +166,11 @@ }, "animateDead" : { "index" : 39, - "anim" : 79, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C01SPE0"] + }, "sounds": { "cast": "ANIMDEAD" }, @@ -163,7 +190,11 @@ }, "sacrifice" : { "index" : 40, - "anim" : 79, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C01SPE0"] + }, "sounds": { "cast": "SACRIF1" }, @@ -184,7 +215,8 @@ }, "teleport" : { "index" : 63, - "anim" : -1, + "targetType" : "CREATURE", + "sounds": { "cast": "TELPTOUT" }, @@ -203,7 +235,8 @@ }, "removeObstacle" : { "index" : 64, - "anim" : -1, + "targetType" : "OBSTACLE", + "sounds": { "cast": "REMOVEOB" }, @@ -218,7 +251,8 @@ }, "clone" : { "index" : 65, - "anim" : -1, + "targetType" : "CREATURE", + "sounds": { "cast": "CLONE" }, @@ -237,7 +271,8 @@ }, "fireElemental" : { "index" : 66, - "anim" : -1, + "targetType" : "NO_TARGET", + "sounds": { "cast": "SUMNELM" }, @@ -252,7 +287,8 @@ }, "earthElemental" : { "index" : 67, - "anim" : -1, + "targetType" : "NO_TARGET", + "sounds": { "cast": "SUMNELM" }, @@ -267,7 +303,8 @@ }, "waterElemental" : { "index" : 68, - "anim" : -1, + "targetType" : "NO_TARGET", + "sounds": { "cast": "SUMNELM" }, @@ -282,7 +319,8 @@ }, "airElemental" : { "index" : 69, - "anim" : -1, + "targetType" : "NO_TARGET", + "sounds": { "cast": "SUMNELM" }, diff --git a/config/spells/timed.json b/config/spells/timed.json index 908be072f..3b565624a 100644 --- a/config/spells/timed.json +++ b/config/spells/timed.json @@ -1,7 +1,11 @@ { "shield" : { "index" : 27, - "anim" : 27, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C13SPE0"] + }, "sounds": { "cast": "SHIELD" }, @@ -27,7 +31,11 @@ }, "airShield" : { "index" : 28, - "anim" : 2, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C01SPA0"] + }, "sounds": { "cast": "AIRSHELD" }, @@ -53,7 +61,11 @@ }, "fireShield" : { "index" : 29, - "anim" : 11, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C05SPF0"] + }, "sounds": { "cast": "FIRESHIE" }, @@ -81,7 +93,11 @@ }, "protectAir" : { "index" : 30, - "anim" : 22, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C11SPE0"] + }, "sounds": { "cast": "PROTECTA" }, @@ -107,7 +123,11 @@ }, "protectFire" : { "index" : 31, - "anim" : 24, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C11SPW0"] + }, "sounds": { "cast": "PROTECTF" }, @@ -133,7 +153,11 @@ }, "protectWater" : { "index" : 32, - "anim" : 23, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C11SPF0"] + }, "sounds": { "cast": "PROTECTW" }, @@ -159,7 +183,11 @@ }, "protectEarth" : { "index" : 33, - "anim" : 26, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C13SPA0"] + }, "sounds": { "cast": "PROTECTE" }, @@ -185,7 +213,11 @@ }, "antiMagic" : { "index" : 34, - "anim" : 5, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C02SPE0"] + }, "sounds": { "cast": "ANTIMAGK" }, @@ -224,7 +256,11 @@ "magicMirror" : { "index" : 36, - "anim" : 3, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C02SPA0"] + }, "sounds": { "cast": "BACKLASH" }, @@ -245,10 +281,13 @@ "positive": true } }, - "bless" : { "index" : 41, - "anim" : 36, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C01SPW"] //C01SPW0 + }, "sounds": { "cast": "BLESS" }, @@ -282,7 +321,11 @@ }, "curse" : { "index" : 42, - "anim" : 40, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C04SPW"]//C04SPW0 + }, "sounds": { "cast": "CURSE" }, @@ -317,7 +360,11 @@ }, "bloodlust" : { "index" : 43, - "anim" : 4, + "targetType" : "CREATURE", + + "animation":{ + "affect":["SP12_"] //??? + }, "sounds": { "cast": "BLOODLUS" }, @@ -360,7 +407,11 @@ }, "precision" : { "index" : 44, - "anim" : 25, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C12SPA0"] + }, "sounds": { "cast": "PRECISON" }, @@ -403,7 +454,11 @@ }, "weakness" : { "index" : 45, - "anim" : 56, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C0ACID"] + }, "sounds": { "cast": "WEAKNESS" }, @@ -446,7 +501,11 @@ }, "stoneSkin" : { "index" : 46, - "anim" : 54, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C16SPE"] //C16SPE0 + }, "sounds": { "cast": "TUFFSKIN" }, @@ -485,8 +544,12 @@ }, "disruptingRay" : { "index" : 47, - "targetType" : "CREATURE", //fix, dont remove - "anim" : 14, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C07SPA1"], + "projectile":[{"defName":"C07SPA0"}]//??? + }, "sounds": { "cast": "DISRUPTR" }, @@ -525,7 +588,11 @@ }, "prayer" : { "index" : 48, - "anim" : 0, + "targetType" : "CREATURE", + + "animation":{ + "affect":[{"defName":"C10SPW", "verticalPosition":"bottom"}] + }, "sounds": { "cast": "PRAYER" }, @@ -588,7 +655,11 @@ }, "mirth" : { "index" : 49, - "anim" : 20, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C09SPW0"] + }, "sounds": { "cast": "MIRTH" }, @@ -636,7 +707,11 @@ }, "sorrow" : { "index" : 50, - "anim" : 30, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C14SPE0"] + }, "sounds": { "cast": "SORROW" }, @@ -684,7 +759,11 @@ }, "fortune" : { "index" : 51, - "anim" : 18, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C09SPA0"] + }, "sounds": { "cast": "FORTUNE" }, @@ -725,7 +804,11 @@ }, "misfortune" : { "index" : 52, - "anim" : 48, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C10SPF0"] + }, "sounds": { "cast": "MISFORT" }, @@ -766,7 +849,11 @@ }, "haste" : { "index" : 53, - "anim" : 31, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C15SPA0"] + }, "sounds": { "cast": "HASTE" }, @@ -811,7 +898,11 @@ }, "slow" : { "index" : 54, - "anim" : 19, + "targetType" : "CREATURE", + + "animation":{ + "affect":[{"defName":"C09SPE0", "verticalPosition":"bottom"}] + }, "sounds": { "cast": "MUCKMIRE" }, @@ -858,7 +949,11 @@ }, "slayer" : { "index" : 55, - "anim" : 28, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C13SPW0"] + }, "sounds": { "cast": "SLAYER" }, @@ -908,7 +1003,11 @@ }, "frenzy" : { "index" : 56, - "anim" : 17, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C08SPF0"] + }, "sounds": { "cast": "FRENZY" }, @@ -946,7 +1045,11 @@ "counterstrike" : { "index" : 58, - "anim" : 7, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C04SPA0"] + }, "sounds": { "cast": "CNTRSTRK" }, @@ -984,7 +1087,11 @@ }, "berserk" : { "index" : 59, - "anim" : 35, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C01SPF"] //C01SPF0 + }, "sounds": { "cast": "BERSERK" }, @@ -1042,7 +1149,11 @@ }, "hypnotize" : { "index" : 60, - "anim" : 21, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C10SPA0"] + }, "sounds": { "cast": "HYPNOTIZ" }, @@ -1099,7 +1210,10 @@ "forgetfulness" : { "index" : 61, "targetType" : "CREATURE", - "anim" : 42, + + "animation":{ + "affect":["C06SPW"]//C06SPW0 + }, "sounds": { "cast": "FORGET" }, @@ -1158,7 +1272,11 @@ }, "blind" : { "index" : 62, - "anim" : 6, + "targetType" : "CREATURE", + + "animation":{ + "affect":["C02SPF0"] + }, "sounds": { "cast": "BLIND" }, diff --git a/lib/BattleState.cpp b/lib/BattleState.cpp index 642fdeb94..9d9583382 100644 --- a/lib/BattleState.cpp +++ b/lib/BattleState.cpp @@ -118,24 +118,6 @@ void BattleInfo::calculateCasualties( std::map *casualties ) const } } -int BattleInfo::calculateSpellDuration( const CSpell * spell, const CGHeroInstance * caster, int usedSpellPower) -{ - if(!caster) - { - if (!usedSpellPower) - return 3; //default duration of all creature spells - else - return usedSpellPower; //use creature spell power - } - switch(spell->id) - { - case SpellID::FRENZY: - return 1; - default: //other spells - return caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER) + caster->valOfBonuses(Bonus::SPELL_DURATION); - } -} - CStack * BattleInfo::generateNewStack(const CStackInstance &base, bool attackerOwned, SlotID slot, BattleHex position) const { int stackID = getIdForNewStack(); @@ -159,36 +141,6 @@ CStack * BattleInfo::generateNewStack(const CStackBasicDescriptor &base, bool at return ret; } -//All spells casted by hero 9resurrection, cure, sacrifice) -ui32 CBattleInfoCallback::calculateHealedHP(const CGHeroInstance * caster, const CSpell * spell, const CStack * stack, const CStack * sacrificedStack) const -{ - bool resurrect = spell->isRisingSpell(); - int healedHealth; - if (spell->id == SpellID::SACRIFICE && sacrificedStack) - healedHealth = (caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER) + sacrificedStack->MaxHealth() + spell->getPower(caster->getSpellSchoolLevel(spell))) * sacrificedStack->count; - else - healedHealth = caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER) * spell->power + spell->getPower(caster->getSpellSchoolLevel(spell)); //??? - healedHealth = calculateSpellBonus(healedHealth, spell, caster, stack); - return std::min(healedHealth, stack->MaxHealth() - stack->firstHPleft + (resurrect ? stack->baseAmount * stack->MaxHealth() : 0)); -} -//Archangel -ui32 CBattleInfoCallback::calculateHealedHP(int healedHealth, const CSpell * spell, const CStack * stack) const -{ - bool resurrect = spell->isRisingSpell(); - return std::min(healedHealth, stack->MaxHealth() - stack->firstHPleft + (resurrect ? stack->baseAmount * stack->MaxHealth() : 0)); -} -//Casted by stack, no hero bonus applied -ui32 CBattleInfoCallback::calculateHealedHP(const CSpell * spell, int usedSpellPower, int spellSchoolLevel, const CStack * stack) const -{ - bool resurrect = spell->isRisingSpell(); - int healedHealth = usedSpellPower * spell->power + spell->getPower(spellSchoolLevel); - return std::min(healedHealth, stack->MaxHealth() - stack->firstHPleft + (resurrect ? stack->baseAmount * stack->MaxHealth() : 0)); -} -bool BattleInfo::resurrects(SpellID spellid) const -{ - return spellid.toSpell()->isRisingSpell(); -} - const CStack * BattleInfo::battleGetStack(BattleHex pos, bool onlyAlive) { CStack * stack = nullptr; diff --git a/lib/BattleState.h b/lib/BattleState.h index 4eb774f4d..dc9c74ae1 100644 --- a/lib/BattleState.h +++ b/lib/BattleState.h @@ -136,12 +136,11 @@ struct DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallb //void getPotentiallyAttackableHexes(AttackableTiles &at, const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos); //hexes around target that could be attacked in melee //std::set getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID); //calculates range of multi-hex attacks //std::set getAttackedHexes(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID); //calculates range of multi-hex attacks - static int calculateSpellDuration(const CSpell * spell, const CGHeroInstance * caster, int usedSpellPower); + CStack * generateNewStack(const CStackInstance &base, bool attackerOwned, SlotID slot, BattleHex position) const; //helper for CGameHandler::setupBattle and spells addign new stacks to the battlefield CStack * generateNewStack(const CStackBasicDescriptor &base, bool attackerOwned, SlotID slot, BattleHex position) const; //helper for CGameHandler::setupBattle and spells addign new stacks to the battlefield int getIdForNewStack() const; //suggest a currently unused ID that'd suitable for generating a new stack //std::pair getNearestStack(const CStack * closest, boost::logic::tribool attackerOwned) const; //if attackerOwned is indetermnate, returened stack is of any owner; hex is the number of hex we should be looking from; returns (nerarest creature, predecessorHex) - bool resurrects(SpellID spellid) const; //TODO: move it to spellHandler? const CGHeroInstance * getHero(PlayerColor player) const; //returns fighting hero that belongs to given player diff --git a/lib/CBattleCallback.cpp b/lib/CBattleCallback.cpp index 9f283a87f..e0cb0c041 100644 --- a/lib/CBattleCallback.cpp +++ b/lib/CBattleCallback.cpp @@ -1565,145 +1565,6 @@ std::vector CBattleInfoCallback::getAttackableBattleHexes() const return attackableBattleHexes; } -ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleIsImmune(const CGHeroInstance * caster, const CSpell * spell, ECastingMode::ECastingMode mode, BattleHex dest) const -{ - RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); - - // Get all stacks at destination hex -> subject of our spell. only alive if not rising spell - TStacks stacks = battleGetStacksIf([=](const CStack * s){ - return s->coversPos(dest) && (spell->isRisingSpell() || s->alive()); - }); - - if(!stacks.empty()) - { - bool allImmune = true; - - ESpellCastProblem::ESpellCastProblem problem; - - for(auto s : stacks) - { - ESpellCastProblem::ESpellCastProblem res = battleStackIsImmune(caster,spell,mode,s); - - if(res == ESpellCastProblem::OK) - { - allImmune = false; - } - else - { - problem = res; - } - } - - if(allImmune) - return problem; - } - else //no target stack on this tile - { - if(spell->getTargetType() == CSpell::CREATURE) - { - if(caster && mode == ECastingMode::HERO_CASTING) //TODO why??? - { - const CSpell::TargetInfo ti = spell->getTargetInfo(caster->getSpellSchoolLevel(spell)); - - if(!ti.massive) - return ESpellCastProblem::WRONG_SPELL_TARGET; - } - else - { - return ESpellCastProblem::WRONG_SPELL_TARGET; - } - - } - } - - return ESpellCastProblem::OK; -} - -ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleStackIsImmune(const CGHeroInstance * caster, const CSpell * spell, ECastingMode::ECastingMode mode, const CStack * subject) const -{ - if (spell->isPositive() && subject->hasBonusOfType(Bonus::RECEPTIVE)) //accept all positive spells - return ESpellCastProblem::OK; - - if (spell->isImmuneBy(subject)) //TODO: move all logic to spellhandler - return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; - - switch (spell->id) //TODO: more general logic for new spells? - { - case SpellID::CLONE: - { - //can't clone already cloned creature - if (vstd::contains(subject->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(spell); - } - else - { - schoolLevel = 3; - } - - if (schoolLevel < 3) - { - int maxLevel = (std::max(schoolLevel, (ui8)1) + 4); - int creLevel = subject->getCreature()->level; - if (maxLevel < creLevel) //tier 1-5 for basic, 1-6 for advanced, any level for expert - return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; - } - } - break; - case SpellID::DISPEL_HELPFUL_SPELLS: - { - TBonusListPtr spellBon = subject->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; - } - } - break; - } - - if (spell->isRisingSpell() && spell->id != SpellID::SACRIFICE) - { - // following does apply to resurrect and animate dead(?) only - // for sacrifice health calculation and health limit check don't matter - - if(subject->count >= subject->baseAmount) - return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; - - if (caster) //FIXME: Archangels can cast immune stack - { - auto maxHealth = calculateHealedHP (caster, spell, subject); - if (maxHealth < subject->MaxHealth()) //must be able to rise at least one full creature - return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; - } - } - else if(spell->id == SpellID::HYPNOTIZE && caster) //do not resist hypnotize casted after attack, for example - { - //TODO: what with other creatures casting hypnotize, Faerie Dragons style? - ui64 subjectHealth = (subject->count - 1) * subject->MaxHealth() + subject->firstHPleft; - //apply 'damage' bonus for hypnotize, including hero specialty - ui64 maxHealth = calculateSpellBonus (caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER) - * spell->power + spell->getPower(caster->getSpellSchoolLevel(spell)), spell, caster, subject); - if (subjectHealth > maxHealth) - return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; - } - - return ESpellCastProblem::OK; -} - ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell( PlayerColor player, const CSpell * spell, ECastingMode::ECastingMode mode ) const { RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); @@ -1746,7 +1607,7 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell auto stacks = spell->isNegative() ? battleAliveStacks(!side) : battleAliveStacks(); for(auto stack : stacks) { - if( ESpellCastProblem::OK == battleStackIsImmune(castingHero, spell, mode, stack)) + if(ESpellCastProblem::OK == spell->isImmuneByStack(castingHero, stack)) { allStacksImmune = false; break; @@ -1790,7 +1651,7 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell for(const CStack * stack : battleGetAllStacks()) //dead stacks will be immune anyway { - bool immune = ESpellCastProblem::OK != battleStackIsImmune(caster, spell, mode, stack); + bool immune = ESpellCastProblem::OK != spell->isImmuneByStack(caster, stack); bool casterStack = stack->owner == caster->getOwner(); if(spell->id == SpellID::SACRIFICE) @@ -1847,8 +1708,6 @@ std::vector CBattleInfoCallback::battleGetPossibleTargets(PlayerColor std::vector ret; RETURN_IF_NOT_BATTLE(ret); - auto mode = ECastingMode::HERO_CASTING; //TODO get rid of this! - switch(spell->getTargetType()) { case CSpell::CREATURE: @@ -1858,7 +1717,7 @@ std::vector CBattleInfoCallback::battleGetPossibleTargets(PlayerColor for(const CStack * stack : battleAliveStacks()) { - bool immune = ESpellCastProblem::OK != battleStackIsImmune(caster, spell, mode, stack); + bool immune = ESpellCastProblem::OK != spell->isImmuneByStack(caster, stack); bool casterStack = stack->owner == caster->getOwner(); if(!immune) @@ -1985,199 +1844,11 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell return ESpellCastProblem::NO_APPROPRIATE_TARGET; } - + const CGHeroInstance * caster = nullptr; if (mode == ECastingMode::HERO_CASTING) - return battleIsImmune(battleGetFightingHero(playerToSide(player)), spell, mode, dest); - else - return battleIsImmune(nullptr, spell, mode, dest); -} - -ui32 CBattleInfoCallback::calculateSpellBonus(ui32 baseDamage, const CSpell * sp, const CGHeroInstance * caster, const CStack * affectedCreature) const -{ - ui32 ret = baseDamage; - //applying sorcery secondary skill - if(caster) - { - ret *= (100.0 + caster->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::SORCERY)) / 100.0; - ret *= (100.0 + caster->valOfBonuses(Bonus::SPELL_DAMAGE) + caster->valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, sp->id.toEnum())) / 100.0; - - if(sp->air) - ret *= (100.0 + caster->valOfBonuses(Bonus::AIR_SPELL_DMG_PREMY)) / 100.0; - else if(sp->fire) //only one type of bonus for Magic Arrow - ret *= (100.0 + caster->valOfBonuses(Bonus::FIRE_SPELL_DMG_PREMY)) / 100.0; - else if(sp->water) - ret *= (100.0 + caster->valOfBonuses(Bonus::WATER_SPELL_DMG_PREMY)) / 100.0; - else if(sp->earth) - ret *= (100.0 + caster->valOfBonuses(Bonus::EARTH_SPELL_DMG_PREMY)) / 100.0; - - if (affectedCreature && affectedCreature->getCreature()->level) //Hero specials like Solmyr, Deemer - ret *= (100. + ((caster->valOfBonuses(Bonus::SPECIAL_SPELL_LEV, sp->id.toEnum()) * caster->level) / affectedCreature->getCreature()->level)) / 100.0; - } - return ret; -} - -ui32 CBattleInfoCallback::calculateSpellDmg( const CSpell * sp, const CGHeroInstance * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower ) const -{ - ui32 ret = 0; //value to return - - //check if spell really does damage - if not, return 0 - if(!sp->isDamageSpell()) - return 0; - - ret = usedSpellPower * sp->power; - ret += sp->getPower(spellSchoolLevel); - - //affected creature-specific part - if(affectedCreature) - { - //applying protections - when spell has more then one elements, only one protection should be applied (I think) - if(sp->air && affectedCreature->hasBonusOfType(Bonus::SPELL_DAMAGE_REDUCTION, 0)) //air spell & protection from air - { - ret *= affectedCreature->valOfBonuses(Bonus::SPELL_DAMAGE_REDUCTION, 0); - ret /= 100; - } - else if(sp->fire && affectedCreature->hasBonusOfType(Bonus::SPELL_DAMAGE_REDUCTION, 1)) //fire spell & protection from fire - { - ret *= affectedCreature->valOfBonuses(Bonus::SPELL_DAMAGE_REDUCTION, 1); - ret /= 100; - } - else if(sp->water && affectedCreature->hasBonusOfType(Bonus::SPELL_DAMAGE_REDUCTION, 2)) //water spell & protection from water - { - ret *= affectedCreature->valOfBonuses(Bonus::SPELL_DAMAGE_REDUCTION, 2); - ret /= 100; - } - else if (sp->earth && affectedCreature->hasBonusOfType(Bonus::SPELL_DAMAGE_REDUCTION, 3)) //earth spell & protection from earth - { - ret *= affectedCreature->valOfBonuses(Bonus::SPELL_DAMAGE_REDUCTION, 3); - ret /= 100; - } - //general spell dmg reduction - //FIXME? - if(sp->air && affectedCreature->hasBonusOfType(Bonus::SPELL_DAMAGE_REDUCTION, -1)) - { - ret *= affectedCreature->valOfBonuses(Bonus::SPELL_DAMAGE_REDUCTION, -1); - ret /= 100; - } - //dmg increasing - if( affectedCreature->hasBonusOfType(Bonus::MORE_DAMAGE_FROM_SPELL, sp->id) ) - { - ret *= 100 + affectedCreature->valOfBonuses(Bonus::MORE_DAMAGE_FROM_SPELL, sp->id.toEnum()); - ret /= 100; - } - } - ret = calculateSpellBonus(ret, sp, caster, affectedCreature); - return ret; -} - -std::set CBattleInfoCallback::getAffectedCreatures(const CSpell * spell, int skillLevel, PlayerColor attackerOwner, BattleHex destinationTile) -{ - std::set attackedCres; //std::set to exclude multiple occurrences of two hex creatures - - const ui8 attackerSide = playerToSide(attackerOwner) == 1; - const auto attackedHexes = spell->rangeInHexes(destinationTile, skillLevel, attackerSide); + caster = battleGetFightingHero(playerToSide(player)); - const CSpell::TargetInfo ti = spell->getTargetInfo(skillLevel); - //TODO: more generic solution for mass spells - if (spell->id == SpellID::CHAIN_LIGHTNING) - { - std::set possibleHexes; - for (auto stack : battleGetAllStacks()) - { - if (stack->isValidTarget()) - { - for (auto hex : stack->getHexes()) - { - possibleHexes.insert (hex); - } - } - } - int targetsOnLevel[4] = {4, 4, 5, 5}; - - BattleHex lightningHex = destinationTile; - for (int i = 0; i < targetsOnLevel[skillLevel]; ++i) - { - auto stack = 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, destinationTile, possibleHexes); - } - } - else if (spell->getLevelInfo(skillLevel).range.size() > 1) //custom many-hex range - { - for(BattleHex hex : attackedHexes) - { - if(const CStack * st = battleGetStackByPos(hex, ti.onlyAlive)) - { - if (spell->id == SpellID::DEATH_CLOUD) //Death Cloud //TODO: fireball and fire immunity - { - if (st->isLiving() || st->coversPos(destinationTile)) //directly hit or alive - { - attackedCres.insert(st); - } - } - else - attackedCres.insert(st); - } - } - } - else if(spell->getTargetType() == CSpell::CREATURE) - { - auto predicate = [=](const CStack * s){ - const bool positiveToAlly = spell->isPositive() && s->owner == attackerOwner; - const bool negativeToEnemy = spell->isNegative() && s->owner != attackerOwner; - 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(destinationTile); - //handle smart targeting - const bool positivenessFlag = !ti.smart || spell->isNeutral() || positiveToAlly || negativeToEnemy; - - return rangeCovers && positivenessFlag && validTarget; - }; - - TStacks stacks = 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 = battleGetStackByPos(hex, ti.onlyAlive)) - attackedCres.insert(st); - } - } - return attackedCres; + return spell->isImmuneAt(this, caster, mode, dest); } const CStack * CBattleInfoCallback::getStackIf(std::function pred) const @@ -2220,93 +1891,126 @@ std::set CBattleInfoCallback:: batteAdjacentCreatures(const CStac SpellID CBattleInfoCallback::getRandomBeneficialSpell(const CStack * subject) const { RETURN_IF_NOT_BATTLE(SpellID::NONE); - std::vector possibleSpells; - - for(const CSpell *spell : VLC->spellh->objects) + //This is complete list. No spells from mods. + //todo: this should be Spellbook of caster Stack + static const std::set allPossibleSpells = { - if (spell->isPositive() && !spell->isRisingSpell()) //only positive and not rising + SpellID::AIR_SHIELD, + SpellID::ANTI_MAGIC, + SpellID::BLESS, + SpellID::BLOODLUST, + SpellID::COUNTERSTRIKE, + SpellID::CURE, + SpellID::FIRE_SHIELD, + SpellID::FORTUNE, + SpellID::HASTE, + SpellID::MAGIC_MIRROR, + SpellID::MIRTH, + SpellID::PRAYER, + SpellID::PRECISION, + SpellID::PROTECTION_FROM_AIR, + SpellID::PROTECTION_FROM_EARTH, + SpellID::PROTECTION_FROM_FIRE, + SpellID::PROTECTION_FROM_WATER, + SpellID::SHIELD, + SpellID::SLAYER, + SpellID::STONE_SKIN + }; + std::vector beneficialSpells; + + auto getAliveEnemy = [=](const std::function & pred) + { + return getStackIf([=](const CStack * stack) { - if (subject->hasBonusFrom(Bonus::SPELL_EFFECT, spell->id) - || battleCanCastThisSpellHere(subject->owner, spell, ECastingMode::CREATURE_ACTIVE_CASTING, subject->position) != ESpellCastProblem::OK) - continue; + return pred(stack) && stack->owner != subject->owner && stack->alive(); + }); + }; - switch (spell->id) - { - case SpellID::SHIELD: - case SpellID::FIRE_SHIELD: // not if all enemy units are shooters - { - auto walker = getStackIf([&](const CStack *stack) //look for enemy, non-shooting stack - { - return stack->owner != subject->owner && !stack->shots; - }); + for(const SpellID spellID : allPossibleSpells) + { + if (subject->hasBonusFrom(Bonus::SPELL_EFFECT, spellID) + //TODO: this ability has special limitations + || battleCanCastThisSpellHere(subject->owner, spellID.toSpell(), ECastingMode::CREATURE_ACTIVE_CASTING, subject->position) != ESpellCastProblem::OK) + continue; - if (!walker) - continue; - } - break; - case SpellID::AIR_SHIELD: //only against active shooters + switch (spellID) + { + case SpellID::SHIELD: + case SpellID::FIRE_SHIELD: // not if all enemy units are shooters + { + auto walker = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack { + return !stack->shots; + }); - auto shooter = getStackIf([&](const CStack *stack) //look for enemy, non-shooting stack - { - return stack->owner != subject->owner && stack->hasBonusOfType(Bonus::SHOOTER) && stack->shots; - }); - if (!shooter) - continue; - } - break; - case SpellID::ANTI_MAGIC: - case SpellID::MAGIC_MIRROR: - { - if (!battleHasHero(subject->attackerOwned)) //only if there is enemy hero - continue; - } - break; - case SpellID::CURE: //only damaged units - what about affected by curse? - { - if (subject->firstHPleft >= subject->MaxHealth()) - continue; - } - break; - case SpellID::BLOODLUST: - { - if (subject->shots) //if can shoot - only if enemy uits are adjacent - continue; - } - break; - case SpellID::PRECISION: - { - if (!(subject->hasBonusOfType(Bonus::SHOOTER) && subject->shots)) - continue; - } - break; - case SpellID::SLAYER://only if monsters are present - { - auto kingMonster = getStackIf([&](const CStack *stack) -> bool //look for enemy, non-shooting stack - { - const auto isKing = Selector::type(Bonus::KING1) - .Or(Selector::type(Bonus::KING2)) - .Or(Selector::type(Bonus::KING3)); - - return stack->owner != subject->owner && stack->hasBonus(isKing); - }); - - if (!kingMonster) - continue; - } - break; - case SpellID::TELEPORT: //issue 1928 - case SpellID::CLONE: //not allowed - continue; - break; + if (!walker) + continue; } - possibleSpells.push_back(spell->id); + break; + case SpellID::AIR_SHIELD: //only against active shooters + { + auto shooter = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack + { + return stack->hasBonusOfType(Bonus::SHOOTER) && stack->shots; + }); + if (!shooter) + continue; + } + break; + case SpellID::ANTI_MAGIC: + case SpellID::MAGIC_MIRROR: + case SpellID::PROTECTION_FROM_AIR: + case SpellID::PROTECTION_FROM_EARTH: + case SpellID::PROTECTION_FROM_FIRE: + case SpellID::PROTECTION_FROM_WATER: + { + const ui8 enemySide = (ui8)subject->attackerOwned; + //todo: only if enemy has spellbook + if (!battleHasHero(enemySide)) //only if there is enemy hero + continue; + } + break; + case SpellID::CURE: //only damaged units + { + //do not cast on affected by debuffs + if (subject->firstHPleft >= subject->MaxHealth()) + continue; + } + break; + case SpellID::BLOODLUST: + { + if (subject->shots) //if can shoot - only if enemy uits are adjacent + continue; + } + break; + case SpellID::PRECISION: + { + if (!(subject->hasBonusOfType(Bonus::SHOOTER) && subject->shots)) + continue; + } + break; + case SpellID::SLAYER://only if monsters are present + { + auto kingMonster = getAliveEnemy([&](const CStack *stack) -> bool //look for enemy, non-shooting stack + { + const auto isKing = Selector::type(Bonus::KING1) + .Or(Selector::type(Bonus::KING2)) + .Or(Selector::type(Bonus::KING3)); + + return stack->hasBonus(isKing); + }); + + if (!kingMonster) + continue; + } + break; } + beneficialSpells.push_back(spellID); } - if(!possibleSpells.empty()) + if(!beneficialSpells.empty()) { - return *RandomGeneratorUtil::nextItem(possibleSpells, gs->getRandomGenerator()); + return *RandomGeneratorUtil::nextItem(beneficialSpells, gs->getRandomGenerator()); } else { diff --git a/lib/CBattleCallback.h b/lib/CBattleCallback.h index 7ab326620..de1bed9f6 100644 --- a/lib/CBattleCallback.h +++ b/lib/CBattleCallback.h @@ -281,21 +281,11 @@ public: ESpellCastProblem::ESpellCastProblem battleCanCastThisSpellHere(PlayerColor player, const CSpell * spell, ECastingMode::ECastingMode mode, BattleHex dest) const; //checks if given player can cast given spell at given tile in given mode ESpellCastProblem::ESpellCastProblem battleCanCreatureCastThisSpell(const CSpell * spell, BattleHex destination) const; //determines if creature can cast a spell here std::vector battleGetPossibleTargets(PlayerColor player, const CSpell *spell) const; - ui32 calculateSpellBonus(ui32 baseDamage, const CSpell * sp, const CGHeroInstance * caster, const CStack * affectedCreature) const; - ui32 calculateSpellDmg(const CSpell * sp, const CGHeroInstance * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const; //calculates damage inflicted by spell - ui32 calculateHealedHP(const CGHeroInstance * caster, const CSpell * spell, const CStack * stack, const CStack * sacrificedStack = nullptr) const; //Sacrifice - ui32 calculateHealedHP(int healedHealth, const CSpell * spell, const CStack * stack) const; //for Archangel - ui32 calculateHealedHP(const CSpell * spell, int usedSpellPower, int spellSchoolLevel, const CStack * stack) const; //healing spells casted by stacks - std::set getAffectedCreatures(const CSpell * s, int skillLevel, PlayerColor attackerOwner, BattleHex destinationTile); //calculates stack affected by given spell SpellID battleGetRandomStackSpell(const CStack * stack, ERandomSpell mode) const; SpellID getRandomBeneficialSpell(const CStack * subject) const; SpellID getRandomCastedSpell(const CStack * caster) const; //called at the beginning of turn for Faerie Dragon - //checks for creature immunity / anything that prevent casting *at given hex* - doesn't take into acount general problems such as not having spellbook or mana points etc. - ESpellCastProblem::ESpellCastProblem battleStackIsImmune(const CGHeroInstance * caster, const CSpell * spell, ECastingMode::ECastingMode mode, const CStack * subject) const; - - const CStack * getStackIf(std::function pred) const; si8 battleHasShootingPenalty(const CStack * stack, BattleHex destHex) @@ -321,17 +311,10 @@ public: AccessibilityInfo getAccesibility(const std::vector &accessibleHexes) const; //given hexes will be marked as accessible std::pair getNearestStack(const CStack * closest, boost::logic::tribool attackerOwned) const; protected: - - //checks for creature immunity / anything that prevent casting *at given hex* - doesn't take into acount general problems such as not having spellbook or mana points etc. - ESpellCastProblem::ESpellCastProblem battleIsImmune(const CGHeroInstance * caster, const CSpell * spell, ECastingMode::ECastingMode mode, BattleHex dest) const; - - ReachabilityInfo getFlyingReachability(const ReachabilityInfo::Parameters ¶ms) const; ReachabilityInfo makeBFS(const AccessibilityInfo &accessibility, const ReachabilityInfo::Parameters ¶ms) const; ReachabilityInfo makeBFS(const CStack *stack) const; //uses default parameters -> stack position and owner's perspective std::set getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const; //get hexes with stopping obstacles (quicksands) - - }; class DLL_LINKAGE CPlayerBattleCallback : public CBattleInfoCallback diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 57fc994c3..0145eb209 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -17,6 +17,7 @@ #include "BattleState.h" // for BattleInfo #include "NetPacks.h" // for InfoWindow #include "CModHandler.h" +#include "CSpellHandler.h" //TODO make clean #define ERROR_VERBOSE_OR_NOT_RET_VAL_IF(cond, verbose, txt, retVal) do {if(cond){if(verbose)logGlobal->errorStream() << BOOST_CURRENT_FUNCTION << ": " << txt; return retVal;}} while(0) @@ -170,19 +171,11 @@ int CGameInfoCallback::estimateSpellDamage(const CSpell * sp, const CGHeroInstan //boost::shared_lock lock(*gs->mx); ERROR_RET_VAL_IF(hero && !canGetFullInfo(hero), "Cannot get info about caster!", -1); - if(!gs->curB) //no battle - { - if (hero) //but we see hero's spellbook - return gs->curB->calculateSpellDmg( - sp, hero, nullptr, hero->getSpellSchoolLevel(sp), hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER)); - else - return 0; //mage guild - } - //gs->getHero(gs->currentPlayer) - //const CGHeroInstance * ourHero = gs->curB->heroes[0]->tempOwner == player ? gs->curB->heroes[0] : gs->curB->heroes[1]; - const CGHeroInstance * ourHero = hero; - return gs->curB->calculateSpellDmg( - sp, ourHero, nullptr, ourHero->getSpellSchoolLevel(sp), ourHero->getPrimSkillLevel(PrimarySkill::SPELL_POWER)); + + if (hero) //we see hero's spellbook + return sp->calculateDamage(hero, nullptr, hero->getSpellSchoolLevel(sp), hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER)); + else + return 0; //mage guild } void CGameInfoCallback::getThievesGuildInfo(SThievesGuildInfo & thi, const CGObjectInstance * obj) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index c079e09c7..125fd6a99 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -83,6 +83,7 @@ set(lib_SRCS VCMI_Lib.cpp VCMIDirs.cpp IHandlerBase.cpp + SpellMechanics.cpp IGameCallback.cpp CGameInfoCallback.cpp diff --git a/lib/CModHandler.cpp b/lib/CModHandler.cpp index 9f5422566..3aaad7af5 100644 --- a/lib/CModHandler.cpp +++ b/lib/CModHandler.cpp @@ -99,6 +99,14 @@ void CIdentifierStorage::requestIdentifier(std::string scope, std::string type, requestIdentifier(ObjectCallback(scope, pair.first, type, pair.second, callback, false)); } +void CIdentifierStorage::requestIdentifier(std::string scope, std::string fullName, const std::function& callback) +{ + auto scopeAndFullName = splitString(fullName, ':'); + auto typeAndName = splitString(scopeAndFullName.second, '.'); + + requestIdentifier(ObjectCallback(scope, scopeAndFullName.first, typeAndName.first, typeAndName.second, callback, false)); +} + void CIdentifierStorage::requestIdentifier(std::string type, const JsonNode & name, const std::function & callback) { auto pair = splitString(name.String(), ':'); // remoteScope:name diff --git a/lib/CModHandler.h b/lib/CModHandler.h index 279e634e7..7d4f00fe5 100644 --- a/lib/CModHandler.h +++ b/lib/CModHandler.h @@ -72,6 +72,8 @@ public: /// request identifier for specific object name. /// Function callback will be called during ID resolution phase of loading void requestIdentifier(std::string scope, std::string type, std::string name, const std::function & callback); + ///fullName = [remoteScope:]type.name + void requestIdentifier(std::string scope, std::string fullName, const std::function & callback); void requestIdentifier(std::string type, const JsonNode & name, const std::function & callback); void requestIdentifier(const JsonNode & name, const std::function & callback); diff --git a/lib/CSpellHandler.cpp b/lib/CSpellHandler.cpp index f8a54db00..ce9306da5 100644 --- a/lib/CSpellHandler.cpp +++ b/lib/CSpellHandler.cpp @@ -10,6 +10,14 @@ #include "CModHandler.h" #include "StringConstants.h" +#include "mapObjects/CGHeroInstance.h" +#include "BattleState.h" +#include "CBattleCallback.h" + +#include "SpellMechanics.h" + + + /* * CSpellHandler.cpp, part of VCMI engine * @@ -23,113 +31,55 @@ namespace SpellConfig { static const std::string LEVEL_NAMES[] = {"none", "basic", "advanced", "expert"}; - + + static const SpellSchoolInfo SCHOOL[4] = + { + { + ESpellSchool::AIR, + Bonus::AIR_SPELL_DMG_PREMY, + Bonus::AIR_IMMUNITY, + "air", + SecondarySkill::AIR_MAGIC, + Bonus::AIR_SPELLS + }, + { + ESpellSchool::FIRE, + Bonus::FIRE_SPELL_DMG_PREMY, + Bonus::FIRE_IMMUNITY, + "fire", + SecondarySkill::FIRE_MAGIC, + Bonus::FIRE_SPELLS + }, + { + ESpellSchool::WATER, + Bonus::WATER_SPELL_DMG_PREMY, + Bonus::WATER_IMMUNITY, + "water", + SecondarySkill::WATER_MAGIC, + Bonus::WATER_SPELLS + }, + { + ESpellSchool::EARTH, + Bonus::EARTH_SPELL_DMG_PREMY, + Bonus::EARTH_IMMUNITY, + "earth", + SecondarySkill::EARTH_MAGIC, + Bonus::EARTH_SPELLS + } + }; } -namespace SRSLPraserHelpers +BattleSpellCastParameters::BattleSpellCastParameters(const BattleInfo* cb) + : spellLvl(0), destination(BattleHex::INVALID), casterSide(0),casterColor(PlayerColor::CANNOT_DETERMINE),caster(nullptr), secHero(nullptr), + usedSpellPower(0),mode(ECastingMode::HERO_CASTING), casterStack(nullptr), selectedStack(nullptr), cb(cb) { - static int XYToHex(int x, int y) - { - return x + 17 * y; - } - - static int XYToHex(std::pair xy) - { - return XYToHex(xy.first, xy.second); - } - - static int hexToY(int battleFieldPosition) - { - return battleFieldPosition/17; - } - - static int hexToX(int battleFieldPosition) - { - int pos = battleFieldPosition - hexToY(battleFieldPosition) * 17; - return pos; - } - - static std::pair 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 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 gotoDir(std::pair xy, int direction) - { - return gotoDir(xy.first, xy.second, direction); - } - - static bool isGoodHex(std::pair xy) - { - return xy.first >=0 && xy.first < 17 && xy.second >= 0 && xy.second < 11; - } - - //helper function for std::set CSpell::rangeInHexes(unsigned int centralHex, ui8 schoolLvl ) const - static std::set getInRange(unsigned int center, int low, int high) - { - std::set ret; - if(low == 0) - { - ret.insert(center); - } - - std::pair 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 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=low) - } - - return ret; - } + } + +///CSpell::LevelInfo CSpell::LevelInfo::LevelInfo() - :description(""),cost(0),power(0),AIValue(0),smartTarget(true),range("0") + :description(""),cost(0),power(0),AIValue(0),smartTarget(true), clearTarget(false), clearAffected(false), range("0") { } @@ -139,22 +89,68 @@ CSpell::LevelInfo::~LevelInfo() } - +///CSpell CSpell::CSpell(): id(SpellID::NONE), level(0), earth(false), water(false), fire(false), air(false), combatSpell(false), creatureAbility(false), positiveness(ESpellPositiveness::NEUTRAL), - mainEffectAnim(-1), defaultProbability(0), isRising(false), isDamage(false), isOffensive(false), - targetType(ETargetType::NO_TARGET) + targetType(ETargetType::NO_TARGET), + mechanics(nullptr) { levels.resize(GameConstants::SPELL_SCHOOL_LEVELS); } CSpell::~CSpell() { + delete mechanics; +} + +void CSpell::afterCast(BattleInfo * battle, const BattleSpellCast * packet) const +{ + mechanics->afterCast(battle, packet); +} + + +void CSpell::battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const +{ + assert(env); + + mechanics->battleCast(env, parameters); +} + +bool CSpell::isCastableBy(const IBonusBearer * caster, bool hasSpellBook, const std::set & spellBook) const +{ + if(!hasSpellBook) + return false; + + const bool inSpellBook = vstd::contains(spellBook, id); + const bool isBonus = caster->hasBonusOfType(Bonus::SPELL, id); + + bool inTome = false; + + forEachSchool([&](const SpellSchoolInfo & cnf, bool & stop) + { + if(caster->hasBonusOfType(cnf.knoledgeBonus)) + { + inTome = stop = true; + } + }); + + if (isSpecialSpell()) + { + if (inSpellBook) + {//hero has this spell in spellbook + logGlobal->errorStream() << "Special spell in spellbook "<hasBonusOfType(Bonus::SPELLS_OF_LEVEL, level); + } } const CSpell::LevelInfo & CSpell::getLevelInfo(const int level) const @@ -168,128 +164,145 @@ const CSpell::LevelInfo & CSpell::getLevelInfo(const int level) const return levels.at(level); } +ui32 CSpell::calculateBonus(ui32 baseDamage, const CGHeroInstance* caster, const CStack* affectedCreature) const +{ + ui32 ret = baseDamage; + //applying sorcery secondary skill + if(caster) + { + ret *= (100.0 + caster->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::SORCERY)) / 100.0; + ret *= (100.0 + caster->valOfBonuses(Bonus::SPELL_DAMAGE) + caster->valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, id.toEnum())) / 100.0; + + forEachSchool([&](const SpellSchoolInfo & cnf, bool & stop) + { + ret *= (100.0 + caster->valOfBonuses(cnf.damagePremyBonus)) / 100.0; + stop = true; //only bonus from one school is used + }); + + if (affectedCreature && affectedCreature->getCreature()->level) //Hero specials like Solmyr, Deemer + ret *= (100. + ((caster->valOfBonuses(Bonus::SPECIAL_SPELL_LEV, id.toEnum()) * caster->level) / affectedCreature->getCreature()->level)) / 100.0; + } + return ret; +} + +ui32 CSpell::calculateDamage(const CGHeroInstance * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const +{ + ui32 ret = 0; //value to return + + //check if spell really does damage - if not, return 0 + if(!isDamageSpell()) + return 0; + + ret = usedSpellPower * power; + ret += getPower(spellSchoolLevel); + + //affected creature-specific part + if(nullptr != affectedCreature) + { + //applying protections - when spell has more then one elements, only one protection should be applied (I think) + + forEachSchool([&](const SpellSchoolInfo & cnf, bool & stop) + { + if(affectedCreature->hasBonusOfType(Bonus::SPELL_DAMAGE_REDUCTION, (ui8)cnf.id)) + { + ret *= affectedCreature->valOfBonuses(Bonus::SPELL_DAMAGE_REDUCTION, (ui8)cnf.id); + ret /= 100; + stop = true;//only bonus from one school is used + } + }); + + //general spell dmg reduction + if(affectedCreature->hasBonusOfType(Bonus::SPELL_DAMAGE_REDUCTION, -1)) + { + ret *= affectedCreature->valOfBonuses(Bonus::SPELL_DAMAGE_REDUCTION, -1); + ret /= 100; + } + //dmg increasing + if(affectedCreature->hasBonusOfType(Bonus::MORE_DAMAGE_FROM_SPELL, id)) + { + ret *= 100 + affectedCreature->valOfBonuses(Bonus::MORE_DAMAGE_FROM_SPELL, id.toEnum()); + ret /= 100; + } + } + ret = calculateBonus(ret, caster, affectedCreature); + return ret; +} + + +ui32 CSpell::calculateHealedHP(const CGHeroInstance* caster, const CStack* stack, const CStack* sacrificedStack) const +{ +//todo: use Mechanics class + int healedHealth; + + if(!isHealingSpell()) + { + logGlobal->errorStream() << "calculateHealedHP called for nonhealing spell "<< name; + return 0; + } + + const int spellPowerSkill = caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER); + const int levelPower = getPower(caster->getSpellSchoolLevel(this)); + + if (id == SpellID::SACRIFICE && sacrificedStack) + healedHealth = (spellPowerSkill + sacrificedStack->MaxHealth() + levelPower) * sacrificedStack->count; + else + healedHealth = spellPowerSkill * power + levelPower; //??? + healedHealth = calculateBonus(healedHealth, caster, stack); + return std::min(healedHealth, stack->MaxHealth() - stack->firstHPleft + (isRisingSpell() ? stack->baseAmount * stack->MaxHealth() : 0)); +} + std::vector CSpell::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const { - using namespace SRSLPraserHelpers; - - std::vector ret; - - if(id == SpellID::FIRE_WALL || id == SpellID::FORCE_FIELD) - { - //Special case - shape of obstacle depends on caster's side - //TODO make it possible through spell_info 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; - } - - - std::string rng = getLevelInfo(schoolLvl).range + ','; //copy + artificial comma for easier handling - - if(rng.size() >= 1 && rng[0] != 'X') //there is at lest one hex in range - { - 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 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; + return mechanics->rangeInHexes(centralHex,schoolLvl,side,outDroppedHexes); } +std::set CSpell::getAffectedStacks(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, PlayerColor casterColor, int spellLvl, BattleHex destination, const CGHeroInstance * caster) const +{ + ISpellMechanics::SpellTargetingContext ctx(this, cb,mode,casterColor,spellLvl,destination); + + std::set attackedCres = mechanics->getAffectedStacks(ctx); + + //now handle immunities + auto predicate = [&, this](const CStack * s)->bool + { + bool hitDirectly = ctx.ti.alwaysHitDirectly && s->coversPos(destination); + bool notImmune = (ESpellCastProblem::OK == isImmuneByStack(caster, s)); + + return !(hitDirectly || notImmune); + }; + vstd::erase_if(attackedCres, predicate); + + return attackedCres; +} + + CSpell::ETargetType CSpell::getTargetType() const { return targetType; } -const CSpell::TargetInfo CSpell::getTargetInfo(const int level) const +CSpell::TargetInfo CSpell::getTargetInfo(const int level) const { - TargetInfo info; - - auto & levelInfo = getLevelInfo(level); - - info.type = getTargetType(); - info.smart = levelInfo.smartTarget; - info.massive = levelInfo.range == "X"; - info.onlyAlive = !isRisingSpell(); - + TargetInfo info(this, level); return info; } +void CSpell::forEachSchool(const std::function& cb) const +{ + bool stop = false; + for(const SpellSchoolInfo & cnf : SpellConfig::SCHOOL) + { + if(school.at(cnf.id)) + { + cb(cnf, stop); + + if(stop) + break; + } + } +} + bool CSpell::isCombatSpell() const { @@ -321,6 +334,10 @@ bool CSpell::isNeutral() const return positiveness == NEUTRAL; } +bool CSpell::isHealingSpell() const +{ + return isRisingSpell() || (id == SpellID::CURE); +} bool CSpell::isRisingSpell() const { @@ -408,33 +425,89 @@ void CSpell::getEffects(std::vector& lst, const int level) const } } -bool CSpell::isImmuneBy(const IBonusBearer* obj) const +ESpellCastProblem::ESpellCastProblem CSpell::isImmuneAt(const CBattleInfoCallback * cb, const CGHeroInstance * caster, ECastingMode::ECastingMode mode, BattleHex destination) const { + // Get all stacks at destination hex. only alive if not rising spell + TStacks stacks = cb->battleGetStacksIf([=](const CStack * s){ + return s->coversPos(destination) && (isRisingSpell() || s->alive()); + }); + + if(!stacks.empty()) + { + bool allImmune = true; + + ESpellCastProblem::ESpellCastProblem problem; + + for(auto s : stacks) + { + ESpellCastProblem::ESpellCastProblem res = isImmuneByStack(caster,s); + + if(res == ESpellCastProblem::OK) + { + allImmune = false; + } + else + { + problem = res; + } + } + + if(allImmune) + return problem; + } + else //no target stack on this tile + { + if(getTargetType() == CSpell::CREATURE) + { + if(caster && mode == ECastingMode::HERO_CASTING) //TODO why??? + { + const CSpell::TargetInfo ti(this, caster->getSpellSchoolLevel(this), mode); + + if(!ti.massive) + return ESpellCastProblem::WRONG_SPELL_TARGET; + } + else + { + return ESpellCastProblem::WRONG_SPELL_TARGET; + } + } + } + + return ESpellCastProblem::OK; +} + + +ESpellCastProblem::ESpellCastProblem CSpell::isImmuneBy(const IBonusBearer* obj) const +{ //todo: use new bonus API //1. Check absolute limiters for(auto b : absoluteLimiters) { if (!obj->hasBonusOfType(b)) - return true; + return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; } //2. Check absolute immunities for(auto b : absoluteImmunities) { if (obj->hasBonusOfType(b)) - return true; + return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; } + + //check receptivity + if (isPositive() && obj->hasBonusOfType(Bonus::RECEPTIVE)) //accept all positive spells + return ESpellCastProblem::OK; //3. Check negation //FIXME: Orb of vulnerability mechanics is not such trivial if(obj->hasBonusOfType(Bonus::NEGATE_ALL_NATURAL_IMMUNITIES)) //Orb of vulnerability - return false; + return ESpellCastProblem::NOT_DECIDED; //4. Check negatable limit for(auto b : limiters) { if (!obj->hasBonusOfType(b)) - return true; + return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; } @@ -442,55 +515,56 @@ bool CSpell::isImmuneBy(const IBonusBearer* obj) const for(auto b : immunities) { if (obj->hasBonusOfType(b)) - return true; + return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; } - auto battleTestElementalImmunity = [&,this](Bonus::BonusType element) -> bool + //6. Check elemental immunities + + ESpellCastProblem::ESpellCastProblem tmp = ESpellCastProblem::NOT_DECIDED; + + forEachSchool([&](const SpellSchoolInfo & cnf, bool & stop) { + auto element = cnf.immunityBonus; + if(obj->hasBonusOfType(element, 0)) //always resist if immune to all spells altogether - return true; + { + tmp = ESpellCastProblem::STACK_IMMUNE_TO_SPELL; + stop = true; + } else if(!isPositive()) //negative or indifferent { if((isDamageSpell() && obj->hasBonusOfType(element, 2)) || obj->hasBonusOfType(element, 1)) - return true; - } - return false; - }; - - //6. Check elemental immunities - if(fire) - { - if(battleTestElementalImmunity(Bonus::FIRE_IMMUNITY)) - return true; - } - if(water) - { - if(battleTestElementalImmunity(Bonus::WATER_IMMUNITY)) - return true; - } - - if(earth) - { - if(battleTestElementalImmunity(Bonus::EARTH_IMMUNITY)) - return true; - } - if(air) - { - if(battleTestElementalImmunity(Bonus::AIR_IMMUNITY)) - return true; - } - + { + tmp = ESpellCastProblem::STACK_IMMUNE_TO_SPELL; + stop = true; + } + } + }); + + if(tmp != ESpellCastProblem::NOT_DECIDED) + return tmp; + TBonusListPtr levelImmunities = obj->getBonuses(Selector::type(Bonus::LEVEL_SPELL_IMMUNITY)); if(obj->hasBonusOfType(Bonus::SPELL_IMMUNITY, id) || ( levelImmunities->size() > 0 && levelImmunities->totalValue() >= level && level)) { - return true; + return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; } - return false; + return ESpellCastProblem::NOT_DECIDED; } +ESpellCastProblem::ESpellCastProblem CSpell::isImmuneByStack(const CGHeroInstance* caster, const CStack* obj) const +{ + const auto immuneResult = mechanics->isImmuneByStack(caster,obj); + + if (ESpellCastProblem::NOT_DECIDED != immuneResult) + return immuneResult; + return ESpellCastProblem::OK; +} + + void CSpell::setIsOffensive(const bool val) { isOffensive = val; @@ -512,6 +586,89 @@ void CSpell::setIsRising(const bool val) } } +void CSpell::setup() +{ + setupMechanics(); + + air = school[ESpellSchool::AIR]; + fire = school[ESpellSchool::FIRE]; + water = school[ESpellSchool::WATER]; + earth = school[ESpellSchool::EARTH]; +} + + +void CSpell::setupMechanics() +{ + if(nullptr != mechanics) + { + logGlobal->errorStream() << "Spell " << this->name << " mechanics already set"; + delete mechanics; + } + + mechanics = ISpellMechanics::createMechanics(this); +} + +///CSpell::AnimationInfo +CSpell::AnimationInfo::AnimationInfo() +{ + +} + +CSpell::AnimationInfo::~AnimationInfo() +{ + +} + +std::string CSpell::AnimationInfo::selectProjectile(const double angle) const +{ + std::string res; + double maximum = 0.0; + + for(const auto & info : projectile) + { + if(info.minimumAngle < angle && info.minimumAngle > maximum) + { + maximum = info.minimumAngle; + res = info.resourceName; + } + } + + return std::move(res); +} + + +///CSpell::TargetInfo +CSpell::TargetInfo::TargetInfo(const CSpell * spell, const int level) +{ + init(spell, level); +} + +CSpell::TargetInfo::TargetInfo(const CSpell * spell, const int level, ECastingMode::ECastingMode mode) +{ + init(spell, level); + if(mode == ECastingMode::ENCHANTER_CASTING) + { + smart = true; //FIXME: not sure about that, this makes all spells smart in this mode + massive = true; + } + else if(mode == ECastingMode::SPELL_LIKE_ATTACK) + { + alwaysHitDirectly = true; + } +} + +void CSpell::TargetInfo::init(const CSpell * spell, const int level) +{ + auto & levelInfo = spell->getLevelInfo(level); + + type = spell->getTargetType(); + smart = levelInfo.smartTarget; + massive = levelInfo.range == "X"; + onlyAlive = !spell->isRisingSpell(); + alwaysHitDirectly = false; + clearAffected = levelInfo.clearAffected; + clearTarget = levelInfo.clearTarget; +} bool DLL_LINKAGE isInScreenRange(const int3 ¢er, const int3 &pos) @@ -523,6 +680,7 @@ bool DLL_LINKAGE isInScreenRange(const int3 ¢er, const int3 &pos) return false; } +///CSpellHandler CSpellHandler::CSpellHandler() { @@ -590,17 +748,7 @@ std::vector CSpellHandler::loadLegacyData(size_t dataSize) for(size_t i = 0; i < GameConstants::SPELL_SCHOOL_LEVELS ; i++) descriptions.push_back(parser.readString()); - std::string attributes = parser.readString(); - - std::string targetType = "NO_TARGET"; - - if(attributes.find("CREATURE_TARGET_1") != std::string::npos - || attributes.find("CREATURE_TARGET_2") != std::string::npos) - targetType = "CREATURE_EXPERT_MASSIVE"; - else if(attributes.find("CREATURE_TARGET") != std::string::npos) - targetType = "CREATURE"; - else if(attributes.find("OBSTACLE_TARGET") != std::string::npos) - targetType = "OBSTACLE"; + parser.readString(); //ignore attributes. All data present in JSON //save parsed level specific data for(size_t i = 0; i < GameConstants::SPELL_SCHOOL_LEVELS; i++) @@ -612,16 +760,6 @@ std::vector CSpellHandler::loadLegacyData(size_t dataSize) level["aiValue"].Float() = AIVals[i]; } - if(targetType == "CREATURE_EXPERT_MASSIVE") - { - lineNode["targetType"].String() = "CREATURE"; - getLevel(3)["range"].String() = "X"; - } - else - { - lineNode["targetType"].String() = targetType; - } - legacyData.push_back(lineNode); @@ -682,11 +820,11 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json) logGlobal->traceStream() << __FUNCTION__ << ": loading spell " << spell->name; const auto schoolNames = json["school"]; - - spell->air = schoolNames["air"].Bool(); - spell->earth = schoolNames["earth"].Bool(); - spell->fire = schoolNames["fire"].Bool(); - spell->water = schoolNames["water"].Bool(); + + for(const SpellSchoolInfo & info : SpellConfig::SCHOOL) + { + spell->school[info.id] = schoolNames[info.jsonName].Bool(); + } spell->level = json["level"].Float(); spell->power = json["power"].Float(); @@ -711,18 +849,16 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json) spell->targetType = CSpell::CREATURE; else if(targetType == "OBSTACLE") spell->targetType = CSpell::OBSTACLE; - - - spell->mainEffectAnim = json["anim"].Float(); + else if(targetType == "LOCATION") + spell->targetType = CSpell::LOCATION; + else + logGlobal->warnStream() << "Spell " << spell->name << ". Target type " << (targetType.empty() ? "empty" : "unknown ("+targetType+")") << ". Assumed NO_TARGET."; for(const auto & counteredSpell: json["counters"].Struct()) if (counteredSpell.second.Bool()) { - JsonNode tmp(JsonNode::DATA_STRING); - tmp.meta = json.meta; - tmp.String() = counteredSpell.first; - - VLC->modh->identifiers.requestIdentifier(tmp,[=](si32 id){ + VLC->modh->identifiers.requestIdentifier(json.meta, counteredSpell.first, [=](si32 id) + { spell->counteredSpells.push_back(SpellID(id)); }); } @@ -805,10 +941,46 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json) spell->iconScenarioBonus = graphicsNode["iconScenarioBonus"].String(); spell->iconScroll = graphicsNode["iconScroll"].String(); - + const JsonNode & animationNode = json["animation"]; + + auto loadAnimationQueue = [&](const std::string & jsonName, CSpell::TAnimationQueue & q) + { + auto queueNode = animationNode[jsonName].Vector(); + for(const JsonNode & item : queueNode) + { + CSpell::TAnimation newItem; + newItem.verticalPosition = VerticalPosition::TOP; + + if(item.getType() == JsonNode::DATA_STRING) + newItem.resourceName = item.String(); + else if(item.getType() == JsonNode::DATA_STRUCT) + { + newItem.resourceName = item["defName"].String(); + + auto vPosStr = item["verticalPosition"].String(); + if("bottom" == vPosStr) + newItem.verticalPosition = VerticalPosition::BOTTOM; + } + q.push_back(newItem); + } + }; + + loadAnimationQueue("affect", spell->animationInfo.affect); + loadAnimationQueue("cast", spell->animationInfo.cast); + loadAnimationQueue("hit", spell->animationInfo.hit); + + const JsonVector & projectile = animationNode["projectile"].Vector(); + + for(const JsonNode & item : projectile) + { + CSpell::ProjectileInfo info; + info.resourceName = item["defName"].String(); + info.minimumAngle = item["minimumAngle"].Float(); + + spell->animationInfo.projectile.push_back(info); + } const JsonNode & soundsNode = json["sounds"]; - spell->castSound = soundsNode["cast"].String(); @@ -822,14 +994,15 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json) CSpell::LevelInfo & levelObject = spell->levels[levelIndex]; - const si32 levelPower = levelNode["power"].Float(); - - levelObject.description = levelNode["description"].String(); - levelObject.cost = levelNode["cost"].Float(); - levelObject.power = levelPower; - levelObject.AIValue = levelNode["aiValue"].Float(); - levelObject.smartTarget = levelNode["targetModifier"]["smart"].Bool(); - levelObject.range = levelNode["range"].String(); + const si32 levelPower = levelObject.power = levelNode["power"].Float(); + + levelObject.description = levelNode["description"].String(); + levelObject.cost = levelNode["cost"].Float(); + levelObject.AIValue = levelNode["aiValue"].Float(); + levelObject.smartTarget = levelNode["targetModifier"]["smart"].Bool(); + levelObject.clearTarget = levelNode["targetModifier"]["clearTarget"].Bool(); + levelObject.clearAffected = levelNode["targetModifier"]["clearAffected"].Bool(); + levelObject.range = levelNode["range"].String(); for(const auto & elem : levelNode["effects"].Struct()) { @@ -857,9 +1030,12 @@ void CSpellHandler::afterLoadFinalization() { //FIXME: it is a bad place for this code, should refactor loadFromJson to know object id during loading for(auto spell: objects) + { for(auto & level: spell->levels) for(auto & bonus: level.effects) bonus.sid = spell->id; + spell->setup(); + } } void CSpellHandler::beforeValidate(JsonNode & object) diff --git a/lib/CSpellHandler.h b/lib/CSpellHandler.h index 48e61b2ae..e74c0c54f 100644 --- a/lib/CSpellHandler.h +++ b/lib/CSpellHandler.h @@ -4,6 +4,7 @@ #include "../lib/ConstTransitivePtr.h" #include "int3.h" #include "GameConstants.h" +#include "BattleHex.h" #include "HeroBonus.h" @@ -17,20 +18,132 @@ * */ +class CSpell; +class ISpellMechanics; + class CLegacyConfigParser; -struct BattleHex; + +class CGHeroInstance; +class CStack; + +class CBattleInfoCallback; +class BattleInfo; + +struct CPackForClient; +struct BattleSpellCast; + +class CRandomGenerator; + +struct SpellSchoolInfo +{ + ESpellSchool id; //backlink + Bonus::BonusType damagePremyBonus; + Bonus::BonusType immunityBonus; + std::string jsonName; + SecondarySkill::ESecondarySkill skill; + Bonus::BonusType knoledgeBonus; +}; + +///callback to be provided by server +class DLL_LINKAGE SpellCastEnvironment +{ +public: + virtual ~SpellCastEnvironment(){}; + virtual void sendAndApply(CPackForClient * info) const = 0; + + virtual CRandomGenerator & getRandomGenerator() const = 0; + virtual void complain(const std::string & problem) const = 0; +}; + +///helper struct +struct DLL_LINKAGE BattleSpellCastParameters +{ +public: + BattleSpellCastParameters(const BattleInfo * cb); + int spellLvl; + BattleHex destination; + ui8 casterSide; + PlayerColor casterColor; + const CGHeroInstance * caster; + const CGHeroInstance * secHero; + int usedSpellPower; + ECastingMode::ECastingMode mode; + const CStack * casterStack; + const CStack * selectedStack; + const BattleInfo * cb; +}; + +enum class VerticalPosition : ui8{TOP, CENTER, BOTTOM}; class DLL_LINKAGE CSpell { +public: + + struct ProjectileInfo + { + ///in radians. Only positive value. Negative angle is handled by vertical flip + double minimumAngle; + + ///resource name + std::string resourceName; + + template void serialize(Handler &h, const int version) + { + h & minimumAngle & resourceName; + } + }; + + struct AnimationItem + { + std::string resourceName; + VerticalPosition verticalPosition; + + template void serialize(Handler &h, const int version) + { + h & resourceName & verticalPosition; + } + }; + + typedef AnimationItem TAnimation; + typedef std::vector TAnimationQueue; + + struct DLL_LINKAGE AnimationInfo + { + AnimationInfo(); + ~AnimationInfo(); + + ///displayed on all affected targets. + TAnimationQueue affect; + + ///displayed on caster. + TAnimationQueue cast; + + ///displayed on target hex. If spell was casted with no target selection displayed on entire battlefield (f.e. ARMAGEDDON) + TAnimationQueue hit; + + ///displayed "between" caster and (first) target. Ignored if spell was casted with no target selection. + ///use selectProjectile to access + std::vector projectile; + + template void serialize(Handler &h, const int version) + { + h & projectile & hit & cast; + } + + std::string selectProjectile(const double angle) const; + } animationInfo; + public: struct LevelInfo { std::string description; //descriptions of spell for skill level - si32 cost; //per skill level: 0 - none, 1 - basic, etc - si32 power; //per skill level: 0 - none, 1 - basic, etc - si32 AIValue; //AI values: per skill level: 0 - none, 1 - basic, etc + si32 cost; + si32 power; + si32 AIValue; bool smartTarget; + bool clearTarget; + bool clearAffected; std::string range; std::vector effects; @@ -41,6 +154,7 @@ public: template void serialize(Handler &h, const int version) { h & description & cost & power & AIValue & smartTarget & range & effects; + h & clearTarget & clearAffected; } }; @@ -52,7 +166,7 @@ public: */ const CSpell::LevelInfo& getLevelInfo(const int level) const; public: - enum ETargetType {NO_TARGET, CREATURE, OBSTACLE}; + enum ETargetType {NO_TARGET, CREATURE, OBSTACLE, LOCATION}; enum ESpellPositiveness {NEGATIVE = -1, NEUTRAL = 0, POSITIVE = 1}; struct TargetInfo @@ -61,6 +175,17 @@ public: bool smart; bool massive; bool onlyAlive; + ///no immunity on primary target (mostly spell-like attack) + bool alwaysHitDirectly; + + bool clearTarget; + bool clearAffected; + + TargetInfo(const CSpell * spell, const int level); + TargetInfo(const CSpell * spell, const int level, ECastingMode::ECastingMode mode); + + private: + void init(const CSpell * spell, const int level); }; SpellID id; @@ -68,10 +193,13 @@ public: std::string name; si32 level; - bool earth; - bool water; - bool fire; - bool air; + bool earth; //deprecated + bool water; //deprecated + bool fire; //deprecated + bool air; //deprecated + + std::map school; //todo: use this instead of separate boolean fields + si32 power; //spell's power std::map probabilities; //% chance to gain for castles @@ -84,12 +212,15 @@ public: CSpell(); ~CSpell(); + + bool isCastableBy(const IBonusBearer * caster, bool hasSpellBook, const std::set & spellBook) const; + std::vector rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes = nullptr ) const; //convert range to specific hexes; last optional out parameter is set to true, if spell would cover unavailable hexes (that are not included in ret) - si16 mainEffectAnim; //main spell effect animation, in AC format (or -1 when none) ETargetType getTargetType() const; //deprecated - const CSpell::TargetInfo getTargetInfo(const int level) const; + CSpell::TargetInfo getTargetInfo(const int level) const; + bool isCombatSpell() const; bool isAdventureSpell() const; @@ -99,16 +230,36 @@ public: bool isNegative() const; bool isNeutral() const; - bool isRisingSpell() const; bool isDamageSpell() const; + bool isHealingSpell() const; + bool isRisingSpell() const; bool isOffensiveSpell() const; bool isSpecialSpell() const; bool hasEffects() const; void getEffects(std::vector &lst, const int level) const; - - bool isImmuneBy(const IBonusBearer *obj) const; + + ///checks for creature immunity / anything that prevent casting *at given hex* - doesn't take into account general problems such as not having spellbook or mana points etc. + ESpellCastProblem::ESpellCastProblem isImmuneAt(const CBattleInfoCallback * cb, const CGHeroInstance * caster, ECastingMode::ECastingMode mode, BattleHex destination) const; + + //internal, for use only by Mechanics classes + ESpellCastProblem::ESpellCastProblem isImmuneBy(const IBonusBearer *obj) const; + + //checks for creature immunity / anything that prevent casting *at given target* - doesn't take into account general problems such as not having spellbook or mana points etc. + ESpellCastProblem::ESpellCastProblem isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const; + + //internal, for use only by Mechanics classes. applying secondary skills + ui32 calculateBonus(ui32 baseDamage, const CGHeroInstance * caster, const CStack * affectedCreature) const; + + ///calculate spell damage on stack taking caster`s secondary skills and affectedCreature`s bonuses into account + ui32 calculateDamage(const CGHeroInstance * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const; + + ///calculate healed HP for all spells casted by hero + ui32 calculateHealedHP(const CGHeroInstance * caster, const CStack * stack, const CStack * sacrificedStack = nullptr) const; + + ///selects from allStacks actually affected stacks + std::set getAffectedStacks(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, PlayerColor casterColor, int spellLvl, BattleHex destination, const CGHeroInstance * caster = nullptr) const; si32 getCost(const int skillLevel) const; @@ -125,6 +276,13 @@ public: si32 getProbability(const TFaction factionId) const; + /** + * Calls cb for each school this spell belongs to + * + * Set stop to true to abort looping + */ + void forEachSchool(const std::function & cb) const; + /** * Returns resource name of icon for SPELL_IMMUNITY bonus */ @@ -134,28 +292,45 @@ public: template void serialize(Handler &h, const int version) { - h & identifier & id & name & level & earth & water & fire & air & power - & probabilities & attributes & combatSpell & creatureAbility & positiveness & counteredSpells & mainEffectAnim; + h & identifier & id & name & level & power + & probabilities & attributes & combatSpell & creatureAbility & positiveness & counteredSpells; h & isRising & isDamage & isOffensive; h & targetType; h & immunities & limiters & absoluteImmunities & absoluteLimiters; h & iconImmune; h & defaultProbability; - h & isSpecial; - h & castSound & iconBook & iconEffect & iconScenarioBonus & iconScroll; + h & levels; + h & school; + h & animationInfo; - h & levels; - + if(!h.saving) + setup(); } friend class CSpellHandler; friend class Graphics; - +public: + ///Server logic. Has write access to GameState via packets. + ///May be executed on client side by (future) non-cheat-proof scripts. + + //void adventureCast() const; + void battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const; + +public: + ///Client-server logic. Has direct write access to GameState. + ///Shall be called (only) when applying packets on BOTH SIDES + + ///implementation of BattleSpellCast applying + void afterCast(BattleInfo * battle, const BattleSpellCast * packet) const; + private: void setIsOffensive(const bool val); void setIsRising(const bool val); - + + //call this after load or deserialization. cant be done in constructor. + void setup(); + void setupMechanics(); private: si32 defaultProbability; @@ -186,10 +361,10 @@ private: std::string castSound; std::vector levels; + + ISpellMechanics * mechanics;//(!) do not serialize }; - - bool DLL_LINKAGE isInScreenRange(const int3 ¢er, const int3 &pos); //for spells like Dimension Door class DLL_LINKAGE CSpellHandler: public CHandlerBase @@ -216,6 +391,7 @@ public: { h & objects ; } + protected: CSpell * loadFromJson(const JsonNode & json) override; }; diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index 08d04d512..c58262b0f 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -87,6 +87,11 @@ CCreature * CreatureID::toCreature() const CSpell * SpellID::toSpell() const { + if(num < 0 || num >= VLC->spellh->objects.size()) + { + logGlobal->errorStream() << "Unable to get spell of invalid ID " << int(num); + return nullptr; + } return VLC->spellh->objects[*this]; } diff --git a/lib/GameConstants.h b/lib/GameConstants.h index e194aed32..805e142d0 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -395,14 +395,19 @@ namespace ESpellCastProblem SECOND_HEROS_SPELL_IMMUNITY, SPELL_LEVEL_LIMIT_EXCEEDED, NO_SPELLS_TO_DISPEL, NO_APPROPRIATE_TARGET, STACK_IMMUNE_TO_SPELL, WRONG_SPELL_TARGET, ONGOING_TACTIC_PHASE, MAGIC_IS_BLOCKED, //For Orb of Inhibition and similar - no casting at all + NOT_DECIDED, INVALID }; } namespace ECastingMode { - enum ECastingMode {HERO_CASTING, AFTER_ATTACK_CASTING, //also includes cast before attack - MAGIC_MIRROR, CREATURE_ACTIVE_CASTING, ENCHANTER_CASTING}; + enum ECastingMode + { + HERO_CASTING, AFTER_ATTACK_CASTING, //also includes cast before attack + MAGIC_MIRROR, CREATURE_ACTIVE_CASTING, ENCHANTER_CASTING, + SPELL_LIKE_ATTACK + }; } namespace EMarketMode @@ -890,6 +895,14 @@ public: ESpellID num; }; +enum class ESpellSchool: ui8 +{ + AIR = 0, + FIRE = 1, + WATER = 2, + EARTH = 3 +}; + ID_LIKE_OPERATORS_DECLS(SpellID, SpellID::ESpellID) // Typedef declarations diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 0dd65e06f..f798a502d 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -274,17 +274,18 @@ struct ChangeSpells : public CPackForClient //109 struct SetMana : public CPackForClient //110 { - SetMana(){type = 110;}; + SetMana(){type = 110;absolute=true;}; void applyCl(CClient *cl); DLL_LINKAGE void applyGs(CGameState *gs); ObjectInstanceID hid; si32 val; + bool absolute; template void serialize(Handler &h, const int version) { - h & val & hid; + h & val & hid & absolute; } }; @@ -1319,16 +1320,18 @@ struct StacksHealedOrResurrected : public CPackForClient //3013 struct BattleStackAttacked : public CPackForClient//3005 { - BattleStackAttacked(){flags = 0; type = 3005;}; + BattleStackAttacked(): + flags(0), spellID(SpellID::NONE){type=3005;}; void applyFirstCl(CClient * cl); //void applyCl(CClient *cl); DLL_LINKAGE void applyGs(CGameState *gs); ui32 stackAttacked, attackerID; ui32 newAmount, newHP, killedAmount, damageAmount; - enum EFlags {KILLED = 1, EFFECT = 2, SECONDARY = 4, REBIRTH = 8, CLONE_KILLED = 16}; - ui8 flags; //uses EFlags (above) + enum EFlags {KILLED = 1, EFFECT = 2/*deprecated */, SECONDARY = 4, REBIRTH = 8, CLONE_KILLED = 16, SPELL_EFFECT = 32 /*, BONUS_EFFECT = 64 */}; + ui32 flags; //uses EFlags (above) ui32 effect; //set only if flag EFFECT is set + SpellID spellID; //only if flag SPELL_EFFECT is set std::vector healedStacks; //used when life drain @@ -1348,6 +1351,11 @@ struct BattleStackAttacked : public CPackForClient//3005 { return flags & SECONDARY; } + ///Attacked with spell (SPELL_LIKE_ATTACK) + bool isSpell() const + { + return flags & SPELL_EFFECT; + } bool willRebirth() const//resurrection, e.g. Phoenix { return flags & REBIRTH; @@ -1360,6 +1368,7 @@ struct BattleStackAttacked : public CPackForClient//3005 { h & stackAttacked & attackerID & newAmount & newHP & flags & killedAmount & damageAmount & effect & healedStacks; + h & spellID; } bool operator<(const BattleStackAttacked &b) const { @@ -1369,15 +1378,17 @@ struct BattleStackAttacked : public CPackForClient//3005 struct BattleAttack : public CPackForClient//3006 { - BattleAttack(){flags = 0; type = 3006;}; + BattleAttack(): flags(0), spellID(SpellID::NONE){type = 3006;}; void applyFirstCl(CClient *cl); DLL_LINKAGE void applyGs(CGameState *gs); void applyCl(CClient *cl); std::vector bsa; ui32 stackAttacking; - ui8 flags; //uses Eflags (below) - enum EFlags{SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32}; + ui32 flags; //uses Eflags (below) + enum EFlags{SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64}; + + SpellID spellID; //for SPELL_LIKE bool shot() const//distance attack - decrease number of shots { @@ -1403,13 +1414,17 @@ struct BattleAttack : public CPackForClient//3006 { return flags & DEATH_BLOW; } + bool spellLike() const + { + return flags & SPELL_LIKE; + } //bool killed() //if target stack was killed //{ // return bsa.killed(); //} template void serialize(Handler &h, const int version) { - h & bsa & stackAttacking & flags; + h & bsa & stackAttacking & flags & spellID; } }; @@ -1439,7 +1454,7 @@ struct EndAction : public CPackForClient//3008 struct BattleSpellCast : public CPackForClient//3009 { - BattleSpellCast(){type = 3009;}; + BattleSpellCast(){type = 3009; casterStack = -1;}; DLL_LINKAGE void applyGs(CGameState *gs); void applyCl(CClient *cl); @@ -1447,16 +1462,15 @@ struct BattleSpellCast : public CPackForClient//3009 ui8 side; //which hero did cast spell: 0 - attacker, 1 - defender ui32 id; //id of spell ui8 skill; //caster's skill level - ui8 spellCost; ui8 manaGained; //mana channeling ability BattleHex tile; //destination tile (may not be set in some global/mass spells std::vector resisted; //ids of creatures that resisted this spell std::set affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure) - CreatureID attackerType;//id of caster to generate console message; -1 if not set (eg. spell casted by artifact) + si32 casterStack;// -1 if not cated by creature, >=0 caster stack ID bool castedByHero; //if true - spell has been casted by hero, otherwise by a creature template void serialize(Handler &h, const int version) { - h & dmgToDisplay & side & id & skill & spellCost & manaGained & tile & resisted & affectedCres & attackerType & castedByHero; + h & dmgToDisplay & side & id & skill & manaGained & tile & resisted & affectedCres & casterStack & castedByHero; } }; @@ -1578,6 +1592,9 @@ struct BattleStackAdded : public CPackForClient //3017 int amount; int pos; int summoned; //if true, remove it afterwards + + ///Actual stack ID, set on apply, do not serialize + int newStackID; template void serialize(Handler &h, const int version) { diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index d237eb16c..09f7fb8a7 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -184,9 +184,16 @@ DLL_LINKAGE void ChangeSpells::applyGs( CGameState *gs ) DLL_LINKAGE void SetMana::applyGs( CGameState *gs ) { - CGHeroInstance *hero = gs->getHero(hid); - vstd::amax(val, 0); //not less than 0 - hero->mana = val; + CGHeroInstance * hero = gs->getHero(hid); + + assert(hero); + + if(absolute) + hero->mana = val; + else + hero->mana += val; + + vstd::amax(hero->mana, 0); //not less than 0 } DLL_LINKAGE void SetMovePoints::applyGs( CGameState *gs ) @@ -1329,44 +1336,10 @@ DLL_LINKAGE void StartAction::applyGs( CGameState *gs ) DLL_LINKAGE void BattleSpellCast::applyGs( CGameState *gs ) { assert(gs->curB); - if (castedByHero) - { - CGHeroInstance * h = gs->curB->battleGetFightingHero(side); - CGHeroInstance * enemy = gs->curB->battleGetFightingHero(!side); - h->mana -= spellCost; - vstd::amax(h->mana, 0); - if (enemy && manaGained) - enemy->mana += manaGained; - if (side < 2) - { - gs->curB->sides[side].castSpellsCount++; - } - } - - //Handle spells removing effects from stacks - const CSpell *spell = SpellID(id).toSpell(); - const bool removeAllSpells = id == SpellID::DISPEL; - const bool removeHelpful = id == SpellID::DISPEL_HELPFUL_SPELLS; - - for(auto stackID : affectedCres) - { - if(vstd::contains(resisted, stackID)) - continue; - - CStack *s = gs->curB->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 bool isPositiveSpell = Selector::positiveSpellEffects(b); - const int spellID = isSpellEffect ? b->sid : -1; - - return (removeHelpful && isPositiveSpell) - || (removeAllSpells && isSpellEffect) - || vstd::contains(spell->counteredSpells, spellID); - }); - } + const CSpell * spell = SpellID(id).toSpell(); + + spell->afterCast(gs->curB, this); } void actualizeEffect(CStack * s, const std::vector & ef) @@ -1591,6 +1564,7 @@ DLL_LINKAGE void BattleStacksRemoved::applyGs( CGameState *gs ) DLL_LINKAGE void BattleStackAdded::applyGs(CGameState *gs) { + newStackID = 0; if (!BattleHex(pos).isValid()) { logNetwork->warnStream() << "No place found for new stack!"; @@ -1604,6 +1578,8 @@ DLL_LINKAGE void BattleStackAdded::applyGs(CGameState *gs) gs->curB->localInitStack(addedStack); gs->curB->stacks.push_back(addedStack); //the stack is not "SUMMONED", it is permanent + + newStackID = addedStack->ID; } DLL_LINKAGE void BattleSetStackProperty::applyGs(CGameState *gs) diff --git a/lib/SpellMechanics.cpp b/lib/SpellMechanics.cpp new file mode 100644 index 000000000..cb49c9775 --- /dev/null +++ b/lib/SpellMechanics.cpp @@ -0,0 +1,1335 @@ +/* + * SpellMechanics.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "SpellMechanics.h" + +#include "CObstacleInstance.h" +#include "mapObjects/CGHeroInstance.h" +#include "BattleState.h" +#include "CRandomGenerator.h" + +#include "NetPacks.h" + +namespace SRSLPraserHelpers +{ + static int XYToHex(int x, int y) + { + return x + GameConstants::BFIELD_WIDTH * y; + } + + static int XYToHex(std::pair 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 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 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 gotoDir(std::pair xy, int direction) + { + return gotoDir(xy.first, xy.second, direction); + } + + static bool isGoodHex(std::pair 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 getInRange(unsigned int center, int low, int high) + { + std::set ret; + if(low == 0) + { + ret.insert(center); + } + + std::pair 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 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=low) + } + + return ret; + } +} + +struct SpellCastContext +{ + SpellCastContext(std::vector & attackedCres, BattleSpellCast & sc, StacksInjured & si): + attackedCres(attackedCres), sc(sc), si(si){}; + std::vector & attackedCres; + BattleSpellCast & sc; + StacksInjured & si; +}; + +class DefaultSpellMechanics: public ISpellMechanics +{ +public: + DefaultSpellMechanics(CSpell * s): ISpellMechanics(s){}; + + std::vector rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool * outDroppedHexes = nullptr) const override; + std::set getAffectedStacks(SpellTargetingContext & ctx) const override; + + ESpellCastProblem::ESpellCastProblem isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const override; + + //bool adventureCast(const SpellCastContext & context) const override; + void battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const override; + + void afterCast(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; + +}; + +class AcidBreathDamageMechnics: public DefaultSpellMechanics +{ +public: + AcidBreathDamageMechnics(CSpell * s): DefaultSpellMechanics(s){}; +protected: + void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; +}; + +class ChainLightningMechanics: public DefaultSpellMechanics +{ +public: + ChainLightningMechanics(CSpell * s): DefaultSpellMechanics(s){}; + std::set 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 afterCast(BattleInfo * battle, const BattleSpellCast * packet) const override; +}; + +class DeathStareMechnics: public DefaultSpellMechanics +{ +public: + DeathStareMechnics(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 afterCast(BattleInfo * battle, const BattleSpellCast * packet) const override; + + ESpellCastProblem::ESpellCastProblem isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const override; +}; + +class DispellMechanics: public DefaultSpellMechanics +{ +public: + DispellMechanics(CSpell * s): DefaultSpellMechanics(s){}; + + void afterCast(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 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; +}; + +///ISpellMechanics +ISpellMechanics::ISpellMechanics(CSpell * s): + owner(s) +{ + +} + +ISpellMechanics * ISpellMechanics::createMechanics(CSpell * s) +{ + switch (s->id) + { + case SpellID::ACID_BREATH_DAMAGE: + return new AcidBreathDamageMechnics(s); + case SpellID::CHAIN_LIGHTNING: + return new ChainLightningMechanics(s); + case SpellID::CLONE: + return new CloneMechanics(s); + case SpellID::CURE: + return new CureMechanics(s); + case SpellID::DEATH_STARE: + return new DeathStareMechnics(s); + case SpellID::DISPEL: + return new DispellMechanics(s); + case SpellID::DISPEL_HELPFUL_SPELLS: + return new DispellHelpfulMechanics(s); + case SpellID::FIRE_WALL: + case SpellID::FORCE_FIELD: + return new WallMechanics(s); + case SpellID::HYPNOTIZE: + return new HypnotizeMechanics(s); + case SpellID::LAND_MINE: + case SpellID::QUICKSAND: + return new ObstacleMechanics(s); + case SpellID::REMOVE_OBSTACLE: + return new RemoveObstacleMechanics(s); + case SpellID::SACRIFICE: + return new SacrificeMechanics(s); + case SpellID::SUMMON_FIRE_ELEMENTAL: + case SpellID::SUMMON_EARTH_ELEMENTAL: + case SpellID::SUMMON_WATER_ELEMENTAL: + case SpellID::SUMMON_AIR_ELEMENTAL: + return new SummonMechanics(s); + case SpellID::TELEPORT: + return new TeleportMechanics(s); + default: + if(s->isRisingSpell()) + return new SpecialRisingSpellMechanics(s); + else + return new DefaultSpellMechanics(s); + } +} + + +///DefaultSpellMechanics +void DefaultSpellMechanics::afterCast(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); + }); + } +} + + +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 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 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); + } +} + +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 (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 (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 (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(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(healedHealth, attackedCre->MaxHealth() - attackedCre->firstHPleft + (resurrect ? attackedCre->baseAmount * attackedCre->MaxHealth() : 0)); + } + } + else + hi.healedHP = owner->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 DefaultSpellMechanics::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const +{ + using namespace SRSLPraserHelpers; + + std::vector 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 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 DefaultSpellMechanics::getAffectedStacks(SpellTargetingContext & ctx) const +{ + std::set 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); +} + +///AcidBreathDamageMechnics +void AcidBreathDamageMechnics::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); + } +} + +///ChainLightningMechanics +std::set ChainLightningMechanics::getAffectedStacks(SpellTargetingContext & ctx) const +{ + std::set attackedCres; + + std::set 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::afterCast(BattleInfo * battle, const BattleSpellCast * packet) const +{ + DefaultSpellMechanics::afterCast(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 + }); + } +} + + +///DeathStareMechnics +void DeathStareMechnics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const +{ + //calculating dmg to display + ctx.sc.dmgToDisplay = parameters.usedSpellPower; + 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::afterCast(BattleInfo * battle, const BattleSpellCast * packet) const +{ + DefaultSpellMechanics::afterCast(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); +} + +///DispellMechanics +void DispellMechanics::afterCast(BattleInfo * battle, const BattleSpellCast * packet) const +{ + DefaultSpellMechanics::afterCast(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(); + 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 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(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 WallMechanics::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool * outDroppedHexes) const +{ + using namespace SRSLPraserHelpers; + + std::vector ret; + + //Special case - shape of obstacle depends on caster's side + //TODO make it possible through spell_info 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 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 = owner->calculateHealedHP(caster, obj); + 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 tiles; + tiles.push_back(parameters.destination); + bsm.tilesToMove = tiles; + bsm.teleporting = true; + env->sendAndApply(&bsm); +} + + diff --git a/lib/SpellMechanics.h b/lib/SpellMechanics.h new file mode 100644 index 000000000..0c3b904f7 --- /dev/null +++ b/lib/SpellMechanics.h @@ -0,0 +1,54 @@ +/* + * SpellMechanics.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#pragma once + +#include "CSpellHandler.h" +#include "BattleHex.h" + +class DLL_LINKAGE ISpellMechanics +{ +public: + + struct DLL_LINKAGE SpellTargetingContext + { + const CBattleInfoCallback * cb; + CSpell::TargetInfo ti; + ECastingMode::ECastingMode mode; + BattleHex destination; + PlayerColor casterColor; + int schoolLvl; + + SpellTargetingContext(const CSpell * s, const CBattleInfoCallback * c, ECastingMode::ECastingMode m, PlayerColor cc, int lvl, BattleHex dest) + : cb(c), ti(s,lvl, m), mode(m), destination(dest), casterColor(cc), schoolLvl(lvl) + {}; + + }; + +public: + ISpellMechanics(CSpell * s); + virtual ~ISpellMechanics(){}; + + virtual std::vector rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes = nullptr) const = 0; + virtual std::set getAffectedStacks(SpellTargetingContext & ctx) const = 0; + + virtual ESpellCastProblem::ESpellCastProblem isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const = 0; + + //virtual bool adventureCast(const SpellCastContext & context) const = 0; + virtual void battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const = 0; + + static ISpellMechanics * createMechanics(CSpell * s); + + virtual void afterCast(BattleInfo * battle, const BattleSpellCast * packet) const = 0; + +protected: + CSpell * owner; +}; + diff --git a/lib/VCMI_lib.cbp b/lib/VCMI_lib.cbp index 5090a46b3..772904f6c 100644 --- a/lib/VCMI_lib.cbp +++ b/lib/VCMI_lib.cbp @@ -185,6 +185,8 @@ + +