diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index c65f30f02..3ad085261 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -103,11 +103,6 @@ void CBattleAI::init(std::shared_ptr CB) CB->unlockGsWhenWaiting = false; } -static bool thereRemainsEnemy() -{ - return !cbc->battleIsFinished(); -} - BattleAction CBattleAI::activeStack( const CStack * stack ) { LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()) ; @@ -136,21 +131,20 @@ BattleAction CBattleAI::activeStack( const CStack * stack ) if(cb->battleCanCastSpell()) attemptCastingSpell(); - if(!thereRemainsEnemy()) - return BattleAction(); + if(auto ret = cbc->battleIsFinished()) + { + //spellcast may finish battle + //send special preudo-action + BattleAction cancel; + cancel.actionType = Battle::CANCEL; + return cancel; + } if(auto action = considerFleeingOrSurrendering()) return *action; - if(cb->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY).empty()) - { - //We apparently won battle by casting spell, return defend... (accessing cb may cause trouble) - return BattleAction::makeDefend(stack); - } - PotentialTargets targets(stack); - if(targets.possibleAttacks.size()) { auto hlp = targets.bestAction(); @@ -358,6 +352,7 @@ struct PossibleSpellcast { const CSpell *spell; BattleHex dest; + si32 value; }; struct CurrentOffensivePotential @@ -430,9 +425,9 @@ void CBattleAI::attemptCastingSpell() std::vector possibleCasts; for(auto spell : possibleSpells) { - for(auto hex : getTargetsToConsider(spell)) + for(auto hex : getTargetsToConsider(spell, hero)) { - PossibleSpellcast ps = {spell, hex}; + PossibleSpellcast ps = {spell, hex, 0}; possibleCasts.push_back(ps); } } @@ -458,7 +453,7 @@ void CBattleAI::attemptCastingSpell() { int damageDealt = 0, damageReceived = 0; - auto stacksSuffering = ps.spell->getAffectedStacks(cb.get(), ECastingMode::HERO_CASTING, playerID, skillLevel, ps.dest, hero); + auto stacksSuffering = ps.spell->getAffectedStacks(cb.get(), ECastingMode::HERO_CASTING, hero, skillLevel, ps.dest); if(stacksSuffering.empty()) return -1; @@ -472,41 +467,52 @@ void CBattleAI::attemptCastingSpell() damageDealt += dmg; } - const int damageDiff = damageDealt - damageReceived; + const int damageDiff = damageDealt - damageReceived * 10; - - LOGFL("Casting %s on hex %d would deal %d damage points among %d stacks.", - ps.spell->name % ps.dest % damageDiff % stacksSuffering.size()); + LOGFL("Casting %s on hex %d would deal { %d %d } damage points among %d stacks.", + ps.spell->name % ps.dest % damageDealt % damageReceived % stacksSuffering.size()); //TODO tactic effect too return damageDiff; } case TIMED_EFFECT: { - StackWithBonuses swb; - swb.stack = cb->battleGetStackByPos(ps.dest); - if(!swb.stack) + auto stacksAffected = ps.spell->getAffectedStacks(cb.get(), ECastingMode::HERO_CASTING, hero, skillLevel, ps.dest); + + if(stacksAffected.empty()) return -1; - Bonus pseudoBonus; - pseudoBonus.sid = ps.spell->id; - pseudoBonus.val = skillLevel; - pseudoBonus.turnsRemain = 1; //TODO - CStack::stackEffectToFeature(swb.bonusesToAdd, pseudoBonus); + int totalGain = 0; - HypotheticChangesToBattleState state; - state.bonusesOfStacks[swb.stack] = &swb; + for(const CStack * sta : stacksAffected) + { + StackWithBonuses swb; + swb.stack = sta; - PotentialTargets pt(swb.stack, state); - auto newValue = pt.bestActionValue(); - auto oldValue = valueOfStack[swb.stack]; - auto gain = newValue - oldValue; - if(swb.stack->owner != playerID) //enemy - gain = -gain; + Bonus pseudoBonus; + pseudoBonus.sid = ps.spell->id; + pseudoBonus.val = skillLevel; + pseudoBonus.turnsRemain = 1; //TODO + CStack::stackEffectToFeature(swb.bonusesToAdd, pseudoBonus); - LOGFL("Casting %s on %s would improve the stack by %d points (from %d to %d)", - ps.spell->name % swb.stack->nodeName() % gain % (oldValue) % (newValue)); + HypotheticChangesToBattleState state; + state.bonusesOfStacks[swb.stack] = &swb; - return gain; + PotentialTargets pt(swb.stack, state); + auto newValue = pt.bestActionValue(); + auto oldValue = valueOfStack[swb.stack]; + auto gain = newValue - oldValue; + if(swb.stack->owner != playerID) //enemy + gain = -gain; + + LOGFL("Casting %s on %s would improve the stack by %d points (from %d to %d)", + ps.spell->name % sta->nodeName() % (gain) % (oldValue) % (newValue)); + + totalGain += gain; + } + + LOGFL("Total gain of cast %s at hex %d is %d", ps.spell->name % (ps.dest.hex) % (totalGain)); + + return totalGain; } default: assert(0); @@ -514,7 +520,15 @@ void CBattleAI::attemptCastingSpell() } }; - auto castToPerform = *vstd::maxElementByFun(possibleCasts, evaluateSpellcast); + for(PossibleSpellcast & psc : possibleCasts) + psc.value = evaluateSpellcast(psc); + + auto pscValue = [] (const PossibleSpellcast &ps) -> int + { + return ps.value; + }; + + auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); LOGFL("Best spell is %s. Will cast.", castToPerform.spell->name); BattleAction spellcast; @@ -527,24 +541,60 @@ void CBattleAI::attemptCastingSpell() cb->battleMakeAction(&spellcast); } -std::vector CBattleAI::getTargetsToConsider( const CSpell *spell ) const +std::vector CBattleAI::getTargetsToConsider(const CSpell * spell, const ISpellCaster * caster) const { - if(spell->getTargetType() == CSpell::NO_TARGET) + const CSpell::TargetInfo targetInfo(spell, caster->getSpellSchoolLevel(spell)); + std::vector ret; + + if(targetInfo.massive || targetInfo.type == CSpell::NO_TARGET) { - //Spell can be cast anywhere, all hexes are potentially considerable. - std::vector ret; - - for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) - if(BattleHex(i).isAvailable()) - ret.push_back(i); - - return ret; + ret.push_back(BattleHex()); } else { - //TODO when massive effect -> doesn't matter where cast - return cbc->battleGetPossibleTargets(playerID, spell); + switch(targetInfo.type) + { + case CSpell::CREATURE: + { + for(const CStack * stack : cbc->battleAliveStacks()) + { + bool immune = ESpellCastProblem::OK != spell->isImmuneByStack(caster, stack); + bool casterStack = stack->owner == caster->getOwner(); + + if(!immune) + switch (spell->positiveness) + { + case CSpell::POSITIVE: + if(casterStack || targetInfo.smart) + ret.push_back(stack->position); + break; + + case CSpell::NEUTRAL: + ret.push_back(stack->position); + break; + + case CSpell::NEGATIVE: + if(!casterStack || targetInfo.smart) + ret.push_back(stack->position); + break; + } + } + } + break; + case CSpell::LOCATION: + { + for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) + if(BattleHex(i).isAvailable()) + ret.push_back(i); + } + break; + + default: + break; + } } + + return ret; } boost::optional CBattleAI::considerFleeingOrSurrendering() diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index b6612145d..e3cc42cbb 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -51,11 +51,11 @@ static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const Battl struct ThreatMap -{ +{ std::array, GameConstants::BFIELD_SIZE> threatMap; // [hexNr] -> enemies able to strike - - const CStack *endangered; - std::array sufferedDamage; + + const CStack *endangered; + std::array sufferedDamage; ThreatMap(const CStack *Endangered); }; @@ -89,7 +89,7 @@ const Val getValOr(const std::map &Map, const Key &key, const Val2 def auto i = Map.find(key); if(i != Map.end()) return i->second; - else + else return defaultValue; } @@ -111,8 +111,8 @@ class CBattleAI : public CBattleGameInterface { int side; std::shared_ptr cb; - - //Previous setting of cb + + //Previous setting of cb bool wasWaitingForRealize, wasUnlockingGs; void print(const std::string &text) const; @@ -148,6 +148,6 @@ public: boost::optional considerFleeingOrSurrendering(); void attemptCastingSpell(); - std::vector getTargetsToConsider(const CSpell *spell) const; + std::vector getTargetsToConsider(const CSpell *spell, const ISpellCaster * caster) const; }; diff --git a/client/battle/CBattleInterface.cpp b/client/battle/CBattleInterface.cpp index 0929505ca..d438db32d 100644 --- a/client/battle/CBattleInterface.cpp +++ b/client/battle/CBattleInterface.cpp @@ -98,7 +98,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet * army1, const CCreatureSe std::shared_ptr att, std::shared_ptr defen) : background(nullptr), queue(nullptr), attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0), activeStack(nullptr), mouseHoveredStack(nullptr), stackToActivate(nullptr), selectedStack(nullptr), previouslyHoveredHex(-1), - currentlyHoveredHex(-1), attackingHex(-1), stackCanCastSpell(false), creatureCasting(false), spellDestSelectMode(false), spellSelMode(NO_LOCATION), spellToCast(nullptr), sp(nullptr), + currentlyHoveredHex(-1), attackingHex(-1), stackCanCastSpell(false), creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr), siegeH(nullptr), attackerInt(att), defenderInt(defen), curInt(att), animIDhelper(0), givenCommand(nullptr), myTurn(false), resWindow(nullptr), moveStarted(false), moveSoundHander(-1), bresult(nullptr) { @@ -1272,7 +1272,7 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc ) } //todo: play custom cast animation - displaySpellCast(spellID, BattleHex::INVALID, false); + displaySpellCast(spellID, BattleHex::INVALID); //playing projectile animation if(sc->tile.isValid()) @@ -1323,12 +1323,9 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc ) } //displaying message in console - std::vector logLines; - - spell.prepareBattleLog(curInt->cb.get(), sc, logLines); - - for(auto line : logLines) - console->addText(line); + for(const auto & line : sc->battleLog) + if(!console->addText(line.toString())) + logGlobal->warn("Too long battle log line"); waitForAnims(); //mana absorption @@ -1369,6 +1366,30 @@ void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) } } +CBattleInterface::PossibleActions CBattleInterface::getCasterAction(const CSpell * spell, const ISpellCaster * caster, ECastingMode::ECastingMode mode) const +{ + PossibleActions spellSelMode = ANY_LOCATION; + + const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode); + + 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) + { + spellSelMode = AIMED_SPELL_CREATURE; + } + else if(ti.type == CSpell::OBSTACLE) + { + spellSelMode = OBSTACLE; + } + + return spellSelMode; +} + void CBattleInterface::castThisSpell(SpellID spellID) { auto ba = new BattleAction; @@ -1381,31 +1402,11 @@ void CBattleInterface::castThisSpell(SpellID spellID) spellDestSelectMode = true; creatureCasting = false; - //choosing possible tragets + //choosing possible targets const CGHeroInstance * castingHero = (attackingHeroInstance->tempOwner == curInt->playerID) ? attackingHeroInstance : defendingHeroInstance; assert(castingHero); // code below assumes non-null hero sp = spellID.toSpell(); - spellSelMode = ANY_LOCATION; - - const CSpell::TargetInfo ti(sp, castingHero->getSpellSchoolLevel(sp)); - - 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) - spellSelMode = selectionTypeByPositiveness(*sp); - else - spellSelMode = ANY_CREATURE; - } - else if(ti.type == CSpell::OBSTACLE) - { - spellSelMode = OBSTACLE; - } + PossibleActions spellSelMode = getCasterAction(sp, castingHero, ECastingMode::HERO_CASTING); if (spellSelMode == NO_LOCATION) //user does not have to select location { @@ -1421,13 +1422,12 @@ void CBattleInterface::castThisSpell(SpellID spellID) } } -void CBattleInterface::displayEffect(ui32 effect, int destTile, bool areaEffect) +void CBattleInterface::displayEffect(ui32 effect, int destTile) { - //todo: recheck areaEffect usage addNewAnim(new CSpellEffectAnimation(this, effect, destTile, 0, 0, false)); } -void CBattleInterface::displaySpellAnimation(const CSpell::TAnimation & animation, BattleHex destinationTile, bool areaEffect) +void CBattleInterface::displaySpellAnimation(const CSpell::TAnimation & animation, BattleHex destinationTile) { if(animation.pause > 0) { @@ -1439,7 +1439,7 @@ void CBattleInterface::displaySpellAnimation(const CSpell::TAnimation & animatio } } -void CBattleInterface::displaySpellCast(SpellID spellID, BattleHex destinationTile, bool areaEffect) +void CBattleInterface::displaySpellCast(SpellID spellID, BattleHex destinationTile) { const CSpell * spell = spellID.toSpell(); @@ -1448,11 +1448,11 @@ void CBattleInterface::displaySpellCast(SpellID spellID, BattleHex destinationTi for(const CSpell::TAnimation & animation : spell->animationInfo.cast) { - displaySpellAnimation(animation, destinationTile, areaEffect); + displaySpellAnimation(animation, destinationTile); } } -void CBattleInterface::displaySpellEffect(SpellID spellID, BattleHex destinationTile, bool areaEffect) +void CBattleInterface::displaySpellEffect(SpellID spellID, BattleHex destinationTile) { const CSpell * spell = spellID.toSpell(); @@ -1461,11 +1461,11 @@ void CBattleInterface::displaySpellEffect(SpellID spellID, BattleHex destination for(const CSpell::TAnimation & animation : spell->animationInfo.affect) { - displaySpellAnimation(animation, destinationTile, areaEffect); + displaySpellAnimation(animation, destinationTile); } } -void CBattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTile, bool areaEffect) +void CBattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTile) { const CSpell * spell = spellID.toSpell(); @@ -1474,7 +1474,7 @@ void CBattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTil for(const CSpell::TAnimation & animation : spell->animationInfo.hit) { - displaySpellAnimation(animation, destinationTile, areaEffect); + displaySpellAnimation(animation, destinationTile); } } @@ -1586,9 +1586,10 @@ void CBattleInterface::activateStack() stackCanCastSpell = true; if(randomSpellcaster) creatureSpellToCast = -1; //spell will be set later on cast - - creatureSpellToCast = curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), s, CBattleInfoCallback::RANDOM_AIMED); //faerie dragon can cast only one spell until their next move + else + creatureSpellToCast = curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), s, CBattleInfoCallback::RANDOM_AIMED); //faerie dragon can cast only one spell until their next move //TODO: what if creature can cast BOTH random genie spell and aimed spell? + //TODO: faerie dragon type spell should be selected by server } else { @@ -1633,26 +1634,15 @@ void CBattleInterface::getPossibleActionsForStack(const CStack * stack) { if (stack->hasBonusOfType (Bonus::SPELLCASTER)) { - //TODO: poll possible spells - const CSpell * spell; - BonusList spellBonuses = *stack->getBonuses (Selector::type(Bonus::SPELLCASTER)); - for (Bonus * spellBonus : spellBonuses) + if(creatureSpellToCast != -1) { - spell = CGI->spellh->objects[spellBonus->subtype]; - switch (spellBonus->subtype) - { - case SpellID::REMOVE_OBSTACLE: - possibleActions.push_back (OBSTACLE); - break; - default: - possibleActions.push_back (selectionTypeByPositiveness (*spell)); - break; - } - + const CSpell * spell = SpellID(creatureSpellToCast).toSpell(); + PossibleActions act = getCasterAction(spell, stack, ECastingMode::CREATURE_ACTIVE_CASTING); + if(act == NO_LOCATION) + logGlobal->error("NO_LOCATION action target is not yet supported for creatures"); + else + possibleActions.push_back(act); } - std::sort(possibleActions.begin(), possibleActions.end()); - auto it = std::unique (possibleActions.begin(), possibleActions.end()); - possibleActions.erase (it, possibleActions.end()); } if (stack->hasBonusOfType (Bonus::RANDOM_SPELLCASTER)) possibleActions.push_back (RANDOM_GENIE_SPELL); @@ -1949,21 +1939,6 @@ void CBattleInterface::bTacticNextStack(const CStack *current /*= nullptr*/) } -CBattleInterface::PossibleActions CBattleInterface::selectionTypeByPositiveness(const CSpell & spell) -{ - switch(spell.positiveness) - { - case CSpell::NEGATIVE : - return HOSTILE_CREATURE_SPELL; - case CSpell::NEUTRAL: - return ANY_CREATURE; - case CSpell::POSITIVE: - return FRIENDLY_CREATURE_SPELL; - } - assert(0); - return NO_LOCATION; //should never happen -} - std::string formatDmgRange(std::pair dmgRange) { if(dmgRange.first != dmgRange.second) @@ -2076,27 +2051,10 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType) legalAction = true; } break; - case ANY_CREATURE: - if (shere && shere->alive() && isCastingPossibleHere (sactive, shere, myNumber)) + case AIMED_SPELL_CREATURE: + if (shere && isCastingPossibleHere (sactive, shere, myNumber)) legalAction = true; break; - case HOSTILE_CREATURE_SPELL: - if (shere && shere->alive() && !ourStack && isCastingPossibleHere (sactive, shere, myNumber)) - legalAction = true; - break; - case FRIENDLY_CREATURE_SPELL: - { - if (isCastingPossibleHere (sactive, shere, myNumber)) //need to be called before sp is determined - { - bool rise = false; //TODO: can you imagine rising hostile creatures? - sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : spellToCast->additionalInfo]; - if (sp && sp->isRisingSpell()) - rise = true; - if (shere && (shere->alive() || rise) && ourStack) - legalAction = true; - } - break; - } case RANDOM_GENIE_SPELL: { if (shere && ourStack && shere != sactive) //only positive spells for other allied creatures @@ -2134,30 +2092,11 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType) notLegal = true; break; case FREE_LOCATION: + legalAction = true; + if(!isCastingPossibleHere(sactive, shere, myNumber)) { - ui8 side = curInt->cb->battleGetMySide(); - auto hero = curInt->cb->battleGetMyHero(); - assert(!creatureCasting); //we assume hero casts this spell - assert(hero); - - legalAction = true; - bool hexesOutsideBattlefield = false; - auto tilesThatMustBeClear = sp->rangeInHexes(myNumber, hero->getSpellSchoolLevel(sp), side, &hexesOutsideBattlefield); - for(BattleHex hex : tilesThatMustBeClear) - { - if(curInt->cb->battleGetStackByPos(hex, true) || !!curInt->cb->battleGetObstacleOnPos(hex, false) - || !hex.isAvailable()) - { - legalAction = false; - notLegal = true; - } - } - - if(hexesOutsideBattlefield) - { - legalAction = false; - notLegal = true; - } + legalAction = false; + notLegal = true; } break; case CATAPULT: @@ -2281,9 +2220,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType) consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % sactive->shots % estDmgText).str(); } break; - case HOSTILE_CREATURE_SPELL: - case FRIENDLY_CREATURE_SPELL: - case ANY_CREATURE: + case AIMED_SPELL_CREATURE: sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : spellToCast->additionalInfo]; //necessary if creature has random Genie spell at same time consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[27]) % sp->name % shere->getName()); //Cast %s on %s switch (sp->id) @@ -2324,7 +2261,6 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType) isCastingPossible = true; break; case FREE_LOCATION: - //cursorFrame = ECursor::SPELLBOOK; consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s isCastingPossible = true; break; @@ -2357,9 +2293,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType) { switch (illegalAction) { - case ANY_CREATURE: - case HOSTILE_CREATURE_SPELL: - case FRIENDLY_CREATURE_SPELL: + case AIMED_SPELL_CREATURE: case RANDOM_GENIE_SPELL: cursorFrame = ECursor::COMBAT_BLOCKED; consoleMsg = CGI->generaltexth->allTexts[23]; @@ -2495,7 +2429,7 @@ bool CBattleInterface::isCastingPossibleHere (const CStack * sactive, const CSta if (sp) { - const ISpellCaster * caster = creatureCasting ? dynamic_cast(sactive) : dynamic_cast(curInt->cb->battleGetMyHero()); + const ISpellCaster * caster = creatureCasting ? static_cast(sactive) : static_cast(curInt->cb->battleGetMyHero()); if(caster == nullptr) { isCastingPossible = false;//just in case @@ -3076,18 +3010,27 @@ void CBattleInterface::showHighlightedHexes(SDL_Surface * to) } if (settings["battle"]["mouseShadow"].Bool()) { - if(spellToCast) //when casting spell + const ISpellCaster * caster = nullptr; + const CSpell * spell = nullptr; + + if(spellToCast)//hero casts spell + { + spell = SpellID(spellToCast->additionalInfo).toSpell(); + caster = activeStack->attackerOwned ? attackingHeroInstance : defendingHeroInstance; + } + else if(creatureSpellToCast >= 0 && stackCanCastSpell && creatureCasting)//stack casts spell + { + spell = SpellID(creatureSpellToCast).toSpell(); + caster = activeStack; + } + + if(caster && spell) //when casting spell { //calculating spell school level - const CSpell & spToCast = *CGI->spellh->objects[spellToCast->additionalInfo]; - ui8 schoolLevel = 0; - - auto caster = activeStack->attackerOwned ? attackingHeroInstance : defendingHeroInstance; - if (caster) - schoolLevel = caster->getSpellSchoolLevel(&spToCast); + ui8 schoolLevel = caster->getSpellSchoolLevel(spell); // printing shaded hex(es) - auto shaded = spToCast.rangeInHexes(currentlyHoveredHex, schoolLevel, curInt->cb->battleGetMySide()); + auto shaded = spell->rangeInHexes(currentlyHoveredHex, schoolLevel, curInt->cb->battleGetMySide()); for(BattleHex shadedHex : shaded) { if ((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH -1)) diff --git a/client/battle/CBattleInterface.h b/client/battle/CBattleInterface.h index dbf33913c..7a700d202 100644 --- a/client/battle/CBattleInterface.h +++ b/client/battle/CBattleInterface.h @@ -114,9 +114,10 @@ class CBattleInterface : public CIntObject INVALID = -1, CREATURE_INFO, MOVE_TACTICS, CHOOSE_TACTICS_STACK, MOVE_STACK, ATTACK, WALK_AND_ATTACK, ATTACK_AND_RETURN, SHOOT, //OPEN_GATE, //we can open castle gate during siege - NO_LOCATION, ANY_LOCATION, FRIENDLY_CREATURE_SPELL, HOSTILE_CREATURE_SPELL, RISING_SPELL, ANY_CREATURE, OBSTACLE, TELEPORT, SACRIFICE, RANDOM_GENIE_SPELL, + NO_LOCATION, ANY_LOCATION, OBSTACLE, TELEPORT, SACRIFICE, RANDOM_GENIE_SPELL, FREE_LOCATION, //used with Force Field and Fire Wall - all tiles affected by spell must be free - CATAPULT, HEAL, RISE_DEMONS + CATAPULT, HEAL, RISE_DEMONS, + AIMED_SPELL_CREATURE }; private: SDL_Surface * background, * menu, * amountNormal, * amountNegative, * amountPositive, * amountEffNeutral, * cellBorders, * backgroundWithHexes; @@ -158,7 +159,6 @@ private: bool stackCanCastSpell; //if true, active stack could possibly cast some target spell bool creatureCasting; //if true, stack currently aims to cats a spell bool spellDestSelectMode; //if true, player is choosing destination for his spell - only for GUI / console - PossibleActions spellSelMode; BattleAction * spellToCast; //spell for which player is choosing destination const CSpell * sp; //spell pointer for convenience si32 creatureSpellToCast; @@ -194,7 +194,7 @@ private: const CBattleInterface * owner; public: const CGTownInstance * town; //besieged town - + SiegeHelper(const CGTownInstance * siegeTown, const CBattleInterface * _owner); //c-tor ~SiegeHelper(); //d-tor @@ -259,6 +259,7 @@ private: void redrawBackgroundWithHexes(const CStack * activeStack); /** End of battle screen blitting methods */ + PossibleActions getCasterAction(const CSpell * spell, const ISpellCaster * caster, ECastingMode::ECastingMode mode) const; public: std::list > pendingAnims; //currently displayed animations void addNewAnim(CBattleAnimation * anim); //adds new anim to pendingAnims @@ -334,20 +335,20 @@ 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(SpellID spellID); //called when player has chosen a spell from spellbook - void displayEffect(ui32 effect, int destTile, bool areaEffect = true); //displays custom effect on the battlefield - - void displaySpellCast(SpellID spellID, BattleHex destinationTile, bool areaEffect = true); //displays spell`s cast animation - 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 displaySpellAnimation(const CSpell::TAnimation & animation, BattleHex destinationTile, bool areaEffect = true); - + void displayEffect(ui32 effect, int destTile); //displays custom effect on the battlefield + + void displaySpellCast(SpellID spellID, BattleHex destinationTile); //displays spell`s cast animation + void displaySpellEffect(SpellID spellID, BattleHex destinationTile); //displays spell`s affected animation + void displaySpellHit(SpellID spellID, BattleHex destinationTile); //displays spell`s affected animation + + void displaySpellAnimation(const CSpell::TAnimation & animation, BattleHex destinationTile); + void battleTriggerEffect(const BattleTriggerEffect & bte); void setBattleCursor(const int myNumber); //really complex and messy, sets attackingHex void endAction(const BattleAction* action); void hideQueue(); void showQueue(); - PossibleActions selectionTypeByPositiveness(const CSpell & spell); + Rect hexPosition(BattleHex hex) const; void handleHex(BattleHex myNumber, int eventType); @@ -365,7 +366,7 @@ public: friend class CPlayerInterface; friend class CButton; friend class CInGameConsole; - + friend class CBattleResultWindow; friend class CBattleHero; friend class CSpellEffectAnimation; diff --git a/lib/BattleState.cpp b/lib/BattleState.cpp index 7a8371312..b1c874d8b 100644 --- a/lib/BattleState.cpp +++ b/lib/BattleState.cpp @@ -1243,6 +1243,19 @@ const PlayerColor CStack::getOwner() const return owner; } +void CStack::getCasterName(MetaString & text) const +{ + //always plural name in case of spell cast. + text.addReplacement(MetaString::CRE_PL_NAMES, type->idNumber.num); +} + +void CStack::getCastDescription(const CSpell * spell, const std::vector & attacked, MetaString & text) const +{ + text.addTxt(MetaString::GENERAL_TXT, 565);//The %s casts %s + //todo: use text 566 for single creature + getCasterName(text); + text.addReplacement(MetaString::SPELL_NAME, spell->id.toEnum()); +} bool CMP_stack::operator()( const CStack* a, const CStack* b ) { diff --git a/lib/BattleState.h b/lib/BattleState.h index 31db8f997..6c6a998b3 100644 --- a/lib/BattleState.h +++ b/lib/BattleState.h @@ -249,6 +249,10 @@ public: const PlayerColor getOwner() const override; + void getCasterName(MetaString & text) const override; + + void getCastDescription(const CSpell * spell, const std::vector & attacked, MetaString & text) const override; + ///stack will be ghost in next battle state update void makeGhost(); diff --git a/lib/CBattleCallback.cpp b/lib/CBattleCallback.cpp index 21e468464..8ddc1d3ed 100644 --- a/lib/CBattleCallback.cpp +++ b/lib/CBattleCallback.cpp @@ -70,7 +70,8 @@ namespace SiegeStuffThatShouldBeMovedToHandlers // <=== TODO std::make_pair(45, EWallPart::INDESTRUCTIBLE_PART), std::make_pair(62, EWallPart::INDESTRUCTIBLE_PART), std::make_pair(112, EWallPart::INDESTRUCTIBLE_PART), - std::make_pair(147, EWallPart::INDESTRUCTIBLE_PART) + std::make_pair(147, EWallPart::INDESTRUCTIBLE_PART), + std::make_pair(165, EWallPart::INDESTRUCTIBLE_PART) }; static EWallPart::EWallPart hexToWallPart(BattleHex hex) @@ -345,9 +346,15 @@ const IBonusBearer * CBattleInfoEssentials::getBattleNode() const return getBattle(); } -ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(PlayerColor player, ECastingMode::ECastingMode mode) const +ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const ISpellCaster * caster, ECastingMode::ECastingMode mode) const { RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); + if(caster == nullptr) + { + logGlobal->errorStream() << "CBattleInfoCallback::battleCanCastSpell: no spellcaster."; + return ESpellCastProblem::INVALID; + } + const PlayerColor player = caster->getOwner(); const ui8 side = playerToSide(player); if(!battleDoWeKnowAbout(side)) { @@ -355,16 +362,17 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(Pla return ESpellCastProblem::INVALID; } + if(battleTacticDist()) + return ESpellCastProblem::ONGOING_TACTIC_PHASE; + switch (mode) { case ECastingMode::HERO_CASTING: { - if(battleTacticDist()) - return ESpellCastProblem::ONGOING_TACTIC_PHASE; if(battleCastSpells(side) > 0) return ESpellCastProblem::ALREADY_CASTED_THIS_TURN; - auto hero = battleGetFightingHero(side); + auto hero = dynamic_cast(caster); if(!hero) return ESpellCastProblem::NO_HERO_TO_CAST_SPELL; @@ -1642,7 +1650,7 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell if(!battleDoWeKnowAbout(side)) return ESpellCastProblem::INVALID; - ESpellCastProblem::ESpellCastProblem genProblem = battleCanCastSpell(player, mode); + ESpellCastProblem::ESpellCastProblem genProblem = battleCanCastSpell(caster, mode); if(genProblem != ESpellCastProblem::OK) return genProblem; @@ -1663,128 +1671,19 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell if(!spell->combatSpell) return ESpellCastProblem::ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL; - const ESpellCastProblem::ESpellCastProblem specificProblem = spell->canBeCast(this, player); + const ESpellCastProblem::ESpellCastProblem specificProblem = spell->canBeCast(this, mode, caster); if(specificProblem != ESpellCastProblem::OK) return specificProblem; - if(spell->isNegative() || spell->hasEffects()) - { - bool allStacksImmune = true; - //we are interested only in enemy stacks when casting offensive spells - //TODO: should hero be able to cast non-smart negative spell if all enemy stacks are immune? - auto stacks = spell->isNegative() ? battleAliveStacks(!side) : battleAliveStacks(); - for(auto stack : stacks) - { - if(ESpellCastProblem::OK == spell->isImmuneByStack(caster, stack)) - { - allStacksImmune = false; - break; - } - } - - if(allStacksImmune) - return ESpellCastProblem::NO_APPROPRIATE_TARGET; - } - - if(battleMaxSpellLevel(side) < spell->level) //effect like Recanter's Cloak or Orb of Inhibition + //effect like Recanter's Cloak. Blocks also passive casting. + //TODO: check creature abilities to block + if(battleMaxSpellLevel(side) < spell->level) return ESpellCastProblem::SPELL_LEVEL_LIMIT_EXCEEDED; - //checking if there exists an appropriate target - switch(spell->getTargetType()) - { - case CSpell::CREATURE: - if(mode == ECastingMode::HERO_CASTING) - { - const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell)); - bool targetExists = false; - - for(const CStack * stack : battleGetAllStacks()) //dead stacks will be immune anyway - { - bool immune = ESpellCastProblem::OK != spell->isImmuneByStack(caster, stack); - bool casterStack = stack->owner == caster->getOwner(); - - if(!immune) - { - switch (spell->positiveness) - { - case CSpell::POSITIVE: - if(casterStack || !ti.smart) - { - targetExists = true; - break; - } - break; - case CSpell::NEUTRAL: - targetExists = true; - break; - case CSpell::NEGATIVE: - if(!casterStack || !ti.smart) - { - targetExists = true; - break; - } - break; - } - } - } - if(!targetExists) - { - return ESpellCastProblem::NO_APPROPRIATE_TARGET; - } - } - break; - case CSpell::OBSTACLE: - break; - } - return ESpellCastProblem::OK; } -std::vector CBattleInfoCallback::battleGetPossibleTargets(PlayerColor player, const CSpell *spell) const -{ - std::vector ret; - RETURN_IF_NOT_BATTLE(ret); - - switch(spell->getTargetType()) - { - case CSpell::CREATURE: - { - const CGHeroInstance * caster = battleGetFightingHero(playerToSide(player)); //TODO - const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell)); - - for(const CStack * stack : battleAliveStacks()) - { - bool immune = ESpellCastProblem::OK != spell->isImmuneByStack(caster, stack); - bool casterStack = stack->owner == caster->getOwner(); - - if(!immune) - switch (spell->positiveness) - { - case CSpell::POSITIVE: - if(casterStack || ti.smart) - ret.push_back(stack->position); - break; - - case CSpell::NEUTRAL: - ret.push_back(stack->position); - break; - - case CSpell::NEGATIVE: - if(!casterStack || ti.smart) - ret.push_back(stack->position); - break; - } - } - } - break; - default: - logGlobal->errorStream() << "FIXME " << __FUNCTION__ << " doesn't work with target type " << spell->getTargetType(); - } - - return ret; -} - ui32 CBattleInfoCallback::battleGetSpellCost(const CSpell * sp, const CGHeroInstance * caster) const { RETURN_IF_NOT_BATTLE(-1); @@ -1821,71 +1720,12 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell logGlobal->errorStream() << "CBattleInfoCallback::battleCanCastThisSpellHere: no spellcaster."; return ESpellCastProblem::INVALID; } - const PlayerColor player = caster->getOwner(); - ESpellCastProblem::ESpellCastProblem moreGeneralProblem = battleCanCastThisSpell(caster, spell, mode); - if(moreGeneralProblem != ESpellCastProblem::OK) - return moreGeneralProblem; - if(spell->getTargetType() == CSpell::OBSTACLE) - { - if(spell->id == SpellID::REMOVE_OBSTACLE) - { - if(auto obstacle = battleGetObstacleOnPos(dest, false)) - { - switch (obstacle->obstacleType) - { - case CObstacleInstance::ABSOLUTE_OBSTACLE: //cliff-like obstacles can't be removed - case CObstacleInstance::MOAT: - return ESpellCastProblem::NO_APPROPRIATE_TARGET; - case CObstacleInstance::USUAL: - return ESpellCastProblem::OK; + ESpellCastProblem::ESpellCastProblem problem = battleCanCastThisSpell(caster, spell, mode); + if(problem != ESpellCastProblem::OK) + return problem; -// //TODO FIRE_WALL only for ADVANCED level casters -// case CObstacleInstance::FIRE_WALL: -// return -// //TODO other magic obstacles for EXPERT -// case CObstacleInstance::QUICKSAND: -// case CObstacleInstance::LAND_MINE: -// case CObstacleInstance::FORCE_FIELD: -// return - default: -// assert(0); - return ESpellCastProblem::OK; - } - } - } - //isObstacleOnTile(dest) - // - // - //TODO - //assert that it's remove obstacle - //rules whether we can remove spell-created obstacle - return ESpellCastProblem::NO_APPROPRIATE_TARGET; - } - - - //get dead stack if we cast resurrection or animate dead - const CStack *deadStack = getStackIf([dest](const CStack *s) { return !s->alive() && s->coversPos(dest); }); - const CStack *aliveStack = getStackIf([dest](const CStack *s) { return s->alive() && s->coversPos(dest);}); - - - if(spell->isRisingSpell()) - { - if(!deadStack && !aliveStack) - return ESpellCastProblem::NO_APPROPRIATE_TARGET; - if(deadStack && deadStack->owner != player) //you can resurrect only your own stacks //FIXME: it includes alive stacks as well - return ESpellCastProblem::NO_APPROPRIATE_TARGET; - } - else if(spell->getTargetType() == CSpell::CREATURE) - { - if(!aliveStack) - return ESpellCastProblem::NO_APPROPRIATE_TARGET; - if(spell->isNegative() && aliveStack->owner == player) - return ESpellCastProblem::NO_APPROPRIATE_TARGET; - if(spell->isPositive() && aliveStack->owner != player) - return ESpellCastProblem::NO_APPROPRIATE_TARGET; - } - return spell->isImmuneAt(this, caster, mode, dest); + return spell->canBeCastAt(this, caster, mode, dest); } const CStack * CBattleInfoCallback::getStackIf(std::function pred) const @@ -2249,7 +2089,16 @@ bool CPlayerBattleCallback::battleCanCastSpell(ESpellCastProblem::ESpellCastProb { RETURN_IF_NOT_BATTLE(false); ASSERT_IF_CALLED_WITH_PLAYER - auto problem = CBattleInfoCallback::battleCanCastSpell(*player, ECastingMode::HERO_CASTING); + + const CGHeroInstance * hero = battleGetMyHero(); + if(!hero) + { + if(outProblem) + *outProblem = ESpellCastProblem::NO_HERO_TO_CAST_SPELL; + return false; + } + + auto problem = CBattleInfoCallback::battleCanCastSpell(hero, ECastingMode::HERO_CASTING); if(outProblem) *outProblem = problem; diff --git a/lib/CBattleCallback.h b/lib/CBattleCallback.h index 138813fd9..ca2677ed7 100644 --- a/lib/CBattleCallback.h +++ b/lib/CBattleCallback.h @@ -212,7 +212,6 @@ public: TStacks battleAliveStacks(ui8 side) const; const CStack * battleGetStackByID(int ID, bool onlyAlive = true) const; //returns stack info by given ID bool battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const; - //ESpellCastProblem::ESpellCastProblem battleCanCastSpell(int player, ECastingMode::ECastingMode mode) const; //Checks if player is able to cast spells (at all) at the moment }; struct DLL_LINKAGE BattleAttackInfo @@ -282,10 +281,9 @@ public: //*** MAGIC si8 battleMaxSpellLevel(ui8 side) const; //calculates minimum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned ui32 battleGetSpellCost(const CSpell * sp, const CGHeroInstance * caster) const; //returns cost of given spell - ESpellCastProblem::ESpellCastProblem battleCanCastSpell(PlayerColor player, ECastingMode::ECastingMode mode) const; //returns true if there are no general issues preventing from casting a spell + ESpellCastProblem::ESpellCastProblem battleCanCastSpell(const ISpellCaster * caster, ECastingMode::ECastingMode mode) const; //returns true if there are no general issues preventing from casting a spell ESpellCastProblem::ESpellCastProblem battleCanCastThisSpell(const ISpellCaster * caster, const CSpell * spell, ECastingMode::ECastingMode mode) const; //checks if given player can cast given spell ESpellCastProblem::ESpellCastProblem battleCanCastThisSpellHere(const ISpellCaster * caster, const CSpell * spell, ECastingMode::ECastingMode mode, BattleHex dest) const; //checks if given player can cast given spell at given tile in given mode - std::vector battleGetPossibleTargets(PlayerColor player, const CSpell *spell) const; SpellID battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const; SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const; diff --git a/lib/CObstacleInstance.cpp b/lib/CObstacleInstance.cpp index 168490e12..0c9d27208 100644 --- a/lib/CObstacleInstance.cpp +++ b/lib/CObstacleInstance.cpp @@ -131,7 +131,6 @@ std::vector SpellCreatedObstacle::getAffectedTiles() const return std::vector(1, pos); case FORCE_FIELD: return SpellID(SpellID::FORCE_FIELD).toSpell()->rangeInHexes(pos, spellLevel, casterSide); - //TODO Fire Wall default: assert(0); return std::vector(); diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 5f10bcfcf..f0f3811ed 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1524,7 +1524,6 @@ struct BattleSpellCast : public CPackForClient//3009 DLL_LINKAGE void applyGs(CGameState *gs); void applyCl(CClient *cl); - si32 dmgToDisplay; //this amount will be displayed as amount of damage dealt by spell ui8 side; //which hero did cast spell: 0 - attacker, 1 - defender ui32 id; //id of spell ui8 skill; //caster's skill level @@ -1534,9 +1533,12 @@ struct BattleSpellCast : public CPackForClient//3009 std::set affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure) si32 casterStack;// -1 if not cated by creature, >=0 caster stack ID bool castByHero; //if true - spell has been cast by hero, otherwise by a creature + + std::vector battleLog; template void serialize(Handler &h, const int version) { - h & dmgToDisplay & side & id & skill & manaGained & tile & customEffects & affectedCres & casterStack & castByHero; + h & side & id & skill & manaGained & tile & customEffects & affectedCres & casterStack & castByHero; + h & battleLog; } }; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 2d22916a5..510008dd8 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1227,6 +1227,15 @@ DLL_LINKAGE void BattleNextRound::applyGs( CGameState *gs ) s->counterAttacksTotalCache = 0; // new turn effects s->battleTurnPassed(); + + if(s->alive() && vstd::contains(s->state, EBattleStackState::CLONED)) + { + //cloned stack has special lifetime marker + //check it after bonuses updated in battleTurnPassed() + + if(!s->hasBonus(Selector::type(Bonus::NONE).And(Selector::source(Bonus::SPELL_EFFECT, SpellID::CLONE)))) + s->makeGhost(); + } } for(auto &obst : gs->curB->obstacles) @@ -1712,6 +1721,13 @@ DLL_LINKAGE void BattleStacksRemoved::applyGs( CGameState *gs ) toRemove->cloneID = -1; } + //cleanup remaining clone links if any + for(CStack * s : gs->curB->stacks) + { + if(s->cloneID == toRemove->ID) + s->cloneID = -1; + } + break; } } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index a3cf0fb8c..85b8c1426 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -954,6 +954,25 @@ const PlayerColor CGHeroInstance::getOwner() const return tempOwner; } +void CGHeroInstance::getCasterName(MetaString & text) const +{ + //FIXME: use local name, MetaString need access to gamestate as hero name is part of map object + + text.addReplacement(name); +} + +void CGHeroInstance::getCastDescription(const CSpell * spell, const std::vector & attacked, MetaString & text) const +{ + const bool singleTarget = attacked.size() == 1; + const int textIndex = singleTarget ? 195 : 196; + + text.addTxt(MetaString::GENERAL_TXT, textIndex); + getCasterName(text); + text.addReplacement(MetaString::SPELL_NAME, spell->id.toEnum()); + if(singleTarget) + text.addReplacement(MetaString::CRE_PL_NAMES, attacked.at(0)->getCreature()->idNumber.num); +} + bool CGHeroInstance::canCastThisSpell(const CSpell * spell) const { if(nullptr == getArt(ArtifactPosition::SPELLBOOK)) diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index dd9d4bffb..dce645af4 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -246,6 +246,9 @@ public: const PlayerColor getOwner() const override; + void getCasterName(MetaString & text) const override; + void getCastDescription(const CSpell * spell, const std::vector & attacked, MetaString & text) const override; + void deserializationFix(); void initObj(CRandomGenerator & rand) override; diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 3241fd25a..5fa1be1ad 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -15,12 +15,94 @@ #include "../CRandomGenerator.h" #include "../mapObjects/CGHeroInstance.h" #include "../NetPacks.h" -#include "../BattleState.h" -#include "../CGameState.h" #include "../CGameInfoCallback.h" #include "../mapping/CMap.h" #include "../CPlayerState.h" +///AdventureSpellMechanics +bool AdventureSpellMechanics::adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const +{ + if(!owner->isAdventureSpell()) + { + env->complain("Attempt to cast non adventure spell in adventure mode"); + return false; + } + + const CGHeroInstance * caster = parameters.caster; + + if(caster->inTownGarrison) + { + env->complain("Attempt to cast an adventure spell in town garrison"); + return false; + } + + const int cost = caster->getSpellCost(owner); + + if(!caster->canCastThisSpell(owner)) + { + env->complain("Hero cannot cast this spell!"); + return false; + } + + if(caster->mana < cost) + { + env->complain("Hero doesn't have enough spell points to cast this spell!"); + return false; + } + + { + AdvmapSpellCast asc; + asc.caster = caster; + asc.spellID = owner->id; + env->sendAndApply(&asc); + } + + switch(applyAdventureEffects(env, parameters)) + { + case ESpellCastResult::OK: + { + SetMana sm; + sm.hid = caster->id; + sm.absolute = false; + sm.val = -cost; + env->sendAndApply(&sm); + return true; + } + break; + case ESpellCastResult::CANCEL: + return true; + } + return false; +} + +ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const +{ + if(owner->hasEffects()) + { + const int schoolLevel = parameters.caster->getSpellSchoolLevel(owner); + + std::vector bonuses; + + owner->getEffects(bonuses, schoolLevel); + + for(Bonus b : bonuses) + { + GiveBonus gb; + gb.id = parameters.caster->id.getNum(); + gb.bonus = b; + env->sendAndApply(&gb); + } + + return ESpellCastResult::OK; + } + else + { + //There is no generic algorithm of adventure cast + env->complain("Unimplemented adventure spell"); + return ESpellCastResult::ERROR; + } +} + ///SummonBoatMechanics ESpellCastResult SummonBoatMechanics::applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const { diff --git a/lib/spells/AdventureSpellMechanics.h b/lib/spells/AdventureSpellMechanics.h index c198290a8..b2ee4bda4 100644 --- a/lib/spells/AdventureSpellMechanics.h +++ b/lib/spells/AdventureSpellMechanics.h @@ -10,47 +10,62 @@ #pragma once -#include "CDefaultSpellMechanics.h" +#include "ISpellMechanics.h" -class ISpellMechanics; -class DefaultSpellMechanics; +enum class ESpellCastResult +{ + OK, + CANCEL,//cast failed but it is not an error + ERROR//internal error occurred +}; -class DLL_LINKAGE SummonBoatMechanics : public DefaultSpellMechanics +class DLL_LINKAGE AdventureSpellMechanics: public IAdventureSpellMechanics { public: - SummonBoatMechanics(CSpell * s): DefaultSpellMechanics(s){}; + AdventureSpellMechanics(CSpell * s): IAdventureSpellMechanics(s){}; + + bool adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override final; +protected: + ///actual adventure cast implementation + virtual ESpellCastResult applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const; +}; + +class DLL_LINKAGE SummonBoatMechanics : public AdventureSpellMechanics +{ +public: + SummonBoatMechanics(CSpell * s): AdventureSpellMechanics(s){}; protected: ESpellCastResult applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override; }; -class DLL_LINKAGE ScuttleBoatMechanics : public DefaultSpellMechanics +class DLL_LINKAGE ScuttleBoatMechanics : public AdventureSpellMechanics { public: - ScuttleBoatMechanics(CSpell * s): DefaultSpellMechanics(s){}; + ScuttleBoatMechanics(CSpell * s): AdventureSpellMechanics(s){}; protected: ESpellCastResult applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override; }; -class DLL_LINKAGE DimensionDoorMechanics : public DefaultSpellMechanics +class DLL_LINKAGE DimensionDoorMechanics : public AdventureSpellMechanics { public: - DimensionDoorMechanics(CSpell * s): DefaultSpellMechanics(s){}; + DimensionDoorMechanics(CSpell * s): AdventureSpellMechanics(s){}; protected: ESpellCastResult applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override; }; -class DLL_LINKAGE TownPortalMechanics : public DefaultSpellMechanics +class DLL_LINKAGE TownPortalMechanics : public AdventureSpellMechanics { public: - TownPortalMechanics(CSpell * s): DefaultSpellMechanics(s){}; + TownPortalMechanics(CSpell * s): AdventureSpellMechanics(s){}; protected: ESpellCastResult applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override; }; -class DLL_LINKAGE ViewMechanics : public DefaultSpellMechanics +class DLL_LINKAGE ViewMechanics : public AdventureSpellMechanics { public: - ViewMechanics(CSpell * s): DefaultSpellMechanics(s){}; + ViewMechanics(CSpell * s): AdventureSpellMechanics(s){}; protected: ESpellCastResult applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override; virtual bool filterObject(const CGObjectInstance * obj, const int spellLevel) const = 0; diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index 83c65bf07..fc0618476 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -41,9 +41,7 @@ void HealingSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, int HealingSpellMechanics::calculateHealedHP(const SpellCastEnvironment* env, const BattleSpellCastParameters& parameters, SpellCastContext& ctx) const { - if(parameters.effectValue != 0) - return parameters.effectValue; //Archangel - return owner->calculateRawEffectValue(parameters.effectLevel, parameters.effectPower); //??? + return parameters.getEffectValue(); } ///AntimagicMechanics @@ -62,12 +60,12 @@ void AntimagicMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast } ///ChainLightningMechanics -std::set ChainLightningMechanics::getAffectedStacks(SpellTargetingContext & ctx) const +std::vector ChainLightningMechanics::calculateAffectedStacks(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const { - std::set attackedCres; + std::vector res; std::set possibleHexes; - for(auto stack : ctx.cb->battleGetAllStacks()) + for(auto stack : cb->battleGetAllStacks()) { if(stack->isValidTarget()) { @@ -82,20 +80,20 @@ std::set ChainLightningMechanics::getAffectedStacks(SpellTargeti BattleHex lightningHex = ctx.destination; for(int i = 0; i < targetsOnLevel[ctx.schoolLvl]; ++i) { - auto stack = ctx.cb->battleGetStackByPos(lightningHex, true); + auto stack = cb->battleGetStackByPos(lightningHex, true); if(!stack) break; - attackedCres.insert (stack); + res.push_back(stack); for(auto hex : stack->getHexes()) { - possibleHexes.erase(hex); //can't hit same place twice + possibleHexes.erase(hex); //can't hit same stack twice } if(possibleHexes.empty()) //not enough targets break; - lightningHex = BattleHex::getClosestTile(stack->attackerOwned, ctx.destination, possibleHexes); + lightningHex = BattleHex::getClosestTile(stack->attackerOwned, lightningHex, possibleHexes); } - return attackedCres; + return res; } ///CloneMechanics @@ -131,6 +129,13 @@ void CloneMechanics::applyBattleEffects(const SpellCastEnvironment * env, const ssp.val = bsa.newStackID; ssp.absolute = 1; env->sendAndApply(&ssp); + + SetStackEffect sse; + sse.stacks.push_back(bsa.newStackID); + Bonus lifeTimeMarker(Bonus::N_TURNS, Bonus::NONE, Bonus::SPELL_EFFECT, 0, owner->id.num); + lifeTimeMarker.turnsRemain = parameters.enchantPower; + sse.effect.push_back(lifeTimeMarker); + env->sendAndApply(&sse); } ESpellCastProblem::ESpellCastProblem CloneMechanics::isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const @@ -165,15 +170,8 @@ ESpellCastProblem::ESpellCastProblem CloneMechanics::isImmuneByStack(const ISpel void CureMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const { DefaultSpellMechanics::applyBattle(battle, packet); - doDispell(battle, packet, [](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 - }); + + doDispell(battle, packet, dispellSelector); } HealingSpellMechanics::EHealLevel CureMechanics::getHealLevel(int effectLevel) const @@ -181,6 +179,25 @@ HealingSpellMechanics::EHealLevel CureMechanics::getHealLevel(int effectLevel) c return EHealLevel::HEAL; } +bool CureMechanics::dispellSelector(const Bonus * b) +{ + if(b->source == Bonus::SPELL_EFFECT) + { + CSpell * sp = SpellID(b->sid).toSpell(); + return sp->isNegative(); + } + return false; //not a spell effect +} + +ESpellCastProblem::ESpellCastProblem CureMechanics::isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const +{ + //Selector method name is ok as cashing string. --AVS + if(!obj->canBeHealed() && !obj->hasBonus(dispellSelector, "CureMechanics::dispellSelector")) + return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; + + return DefaultSpellMechanics::isImmuneByStack(caster, obj); +} + ///DispellMechanics void DispellMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const { @@ -218,8 +235,6 @@ ESpellCastProblem::ESpellCastProblem DispellMechanics::isImmuneByStack(const ISp void DispellMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const { - DefaultSpellMechanics::applyBattleEffects(env, parameters, ctx); - if(parameters.spellLvl > 2) { //expert DISPELL also removes spell-created obstacles @@ -326,8 +341,14 @@ void EarthquakeMechanics::applyBattleEffects(const SpellCastEnvironment * env, c env->sendAndApply(&ca); } -ESpellCastProblem::ESpellCastProblem EarthquakeMechanics::canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const +ESpellCastProblem::ESpellCastProblem EarthquakeMechanics::canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const { + if(mode == ECastingMode::AFTER_ATTACK_CASTING || mode == ECastingMode::SPELL_LIKE_ATTACK || mode == ECastingMode::MAGIC_MIRROR) + { + logGlobal->warn("Invalid spell cast attempt: spell %s, mode %d", owner->name, mode); //should not even try to do it + return ESpellCastProblem::INVALID; + } + if(nullptr == cb->battleGetDefendedTown()) { return ESpellCastProblem::NO_APPROPRIATE_TARGET; @@ -338,17 +359,27 @@ ESpellCastProblem::ESpellCastProblem EarthquakeMechanics::canBeCast(const CBattl return ESpellCastProblem::NO_APPROPRIATE_TARGET; } - CSpell::TargetInfo ti(owner, 0);//TODO: use real spell level + CSpell::TargetInfo ti(owner, caster->getSpellSchoolLevel(owner)); if(ti.smart) { //if spell targeting is smart, then only attacker can use it - if(cb->playerToSide(player) != 0) + if(cb->playerToSide(caster->getOwner()) != 0) return ESpellCastProblem::NO_APPROPRIATE_TARGET; } + const auto attackableBattleHexes = cb->getAttackableBattleHexes(); + + if(attackableBattleHexes.empty()) + return ESpellCastProblem::NO_APPROPRIATE_TARGET; + return ESpellCastProblem::OK; } +bool EarthquakeMechanics::requiresCreatureTarget() const +{ + return false; +} + ///HypnotizeMechanics ESpellCastProblem::ESpellCastProblem HypnotizeMechanics::isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const { @@ -366,105 +397,115 @@ ESpellCastProblem::ESpellCastProblem HypnotizeMechanics::isImmuneByStack(const I } ///ObstacleMechanics -void ObstacleMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const +ESpellCastProblem::ESpellCastProblem ObstacleMechanics::canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const { - auto placeObstacle = [&, this](const BattleHex & pos) - { - static int obstacleIdToGive = parameters.cb->obstacles.size() - ? (parameters.cb->obstacles.back()->uniqueID+1) - : 0; + ui8 side = cb->playerToSide(ctx.caster->getOwner()); - auto obstacle = std::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); - } + bool hexesOutsideBattlefield = false; - obstacle->pos = pos; - obstacle->casterSide = parameters.casterSide; - obstacle->ID = owner->id; - obstacle->spellLevel = parameters.effectLevel; - obstacle->casterSpellPower = parameters.effectPower; - obstacle->uniqueID = obstacleIdToGive++; + auto tilesThatMustBeClear = owner->rangeInHexes(ctx.destination, ctx.schoolLvl, side, &hexesOutsideBattlefield); - BattleObstaclePlaced bop; - bop.obstacle = obstacle; - env->sendAndApply(&bop); - }; + for(const BattleHex & hex : tilesThatMustBeClear) + if(!isHexAviable(cb, hex, ctx.ti.clearAffected)) + return ESpellCastProblem::NO_APPROPRIATE_TARGET; - const BattleHex destination = parameters.getFirstDestinationHex(); + if(hexesOutsideBattlefield) + return ESpellCastProblem::NO_APPROPRIATE_TARGET; - 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); - } - RandomGeneratorUtil::randomShuffle(availableTiles, env->getRandomGenerator()); - - 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: - if(!destination.isValid()) - { - env->complain("Invalid destination for FORCE_FIELD"); - return; - } - placeObstacle(destination); - break; - case SpellID::FIRE_WALL: - { - if(!destination.isValid()) - { - env->complain("Invalid destination for FIRE_WALL"); - return; - } - //fire wall is build from multiple obstacles - one fire piece for each affected hex - auto affectedHexes = owner->rangeInHexes(destination, parameters.spellLvl, parameters.casterSide); - for(BattleHex hex : affectedHexes) - placeObstacle(hex); - } - break; - default: - assert(0); - } + return ESpellCastProblem::OK; } +bool ObstacleMechanics::isHexAviable(const CBattleInfoCallback * cb, const BattleHex & hex, const bool mustBeClear) +{ + if(!hex.isAvailable()) + return false; + + if(!mustBeClear) + return true; + + if(cb->battleGetStackByPos(hex, true) || !!cb->battleGetObstacleOnPos(hex, false)) + return false; + + if(nullptr != cb->battleGetDefendedTown() && CGTownInstance::NONE != cb->battleGetDefendedTown()->fortLevel()) + { + EWallPart::EWallPart part = cb->battleHexToWallPart(hex); + + if(part == EWallPart::INVALID) + return true;//no fortification here + else if(static_cast(part) < 0) + return false;//indestuctible part (cant be checked by battleGetWallState) + else if(part == EWallPart::BOTTOM_TOWER || part == EWallPart::UPPER_TOWER) + return false;//destructible, but should not be available + else if(cb->battleGetWallState(part) != EWallState::DESTROYED && cb->battleGetWallState(part) != EWallState::NONE) + return false; + } + + return true; +} + +void ObstacleMechanics::placeObstacle(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, const BattleHex & pos) const +{ + const int obstacleIdToGive = parameters.cb->obstacles.size()+1; + + auto obstacle = std::make_shared(); + setupObstacle(obstacle.get()); + + obstacle->pos = pos; + obstacle->casterSide = parameters.casterSide; + obstacle->ID = owner->id; + obstacle->spellLevel = parameters.effectLevel; + obstacle->casterSpellPower = parameters.effectPower; + obstacle->uniqueID = obstacleIdToGive; + + BattleObstaclePlaced bop; + bop.obstacle = obstacle; + env->sendAndApply(&bop); +} + +///PatchObstacleMechanics +void PatchObstacleMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const +{ + std::vector availableTiles; + for(int i = 0; i < GameConstants::BFIELD_SIZE; i += 1) + { + BattleHex hex = i; + if(isHexAviable(parameters.cb, hex, true)) + availableTiles.push_back(hex); + } + RandomGeneratorUtil::randomShuffle(availableTiles, env->getRandomGenerator()); + 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(env, parameters, availableTiles.at(i)); +} + +///LandMineMechanics +bool LandMineMechanics::requiresCreatureTarget() const +{ + return true; +} + +void LandMineMechanics::setupObstacle(SpellCreatedObstacle * obstacle) const +{ + obstacle->obstacleType = CObstacleInstance::LAND_MINE; + obstacle->turnsRemaining = -1; + obstacle->visibleForAnotherSide = false; +} + +///QuicksandMechanics +bool QuicksandMechanics::requiresCreatureTarget() const +{ + return false; +} + +void QuicksandMechanics::setupObstacle(SpellCreatedObstacle * obstacle) const +{ + obstacle->obstacleType = CObstacleInstance::QUICKSAND; + obstacle->turnsRemaining = -1; + obstacle->visibleForAnotherSide = false; +} ///WallMechanics std::vector WallMechanics::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool * outDroppedHexes) const @@ -503,19 +544,136 @@ std::vector WallMechanics::rangeInHexes(BattleHex centralHex, ui8 sch return ret; } +///FireWallMechanics +bool FireWallMechanics::requiresCreatureTarget() const +{ + return true; +} + +void FireWallMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const +{ + const BattleHex destination = parameters.getFirstDestinationHex(); + + if(!destination.isValid()) + { + env->complain("Invalid destination for FIRE_WALL"); + return; + } + //firewall is build from multiple obstacles - one fire piece for each affected hex + auto affectedHexes = owner->rangeInHexes(destination, parameters.spellLvl, parameters.casterSide); + for(BattleHex hex : affectedHexes) + placeObstacle(env, parameters, hex); +} + +void FireWallMechanics::setupObstacle(SpellCreatedObstacle * obstacle) const +{ + obstacle->obstacleType = CObstacleInstance::FIRE_WALL; + obstacle->turnsRemaining = 2; + obstacle->visibleForAnotherSide = true; +} + +///ForceFieldMechanics +bool ForceFieldMechanics::requiresCreatureTarget() const +{ + return false; +} + +void ForceFieldMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const +{ + const BattleHex destination = parameters.getFirstDestinationHex(); + + if(!destination.isValid()) + { + env->complain("Invalid destination for FORCE_FIELD"); + return; + } + placeObstacle(env, parameters, destination); +} + +void ForceFieldMechanics::setupObstacle(SpellCreatedObstacle * obstacle) const +{ + obstacle->obstacleType = CObstacleInstance::FORCE_FIELD; + obstacle->turnsRemaining = 2; + obstacle->visibleForAnotherSide = true; +} + ///RemoveObstacleMechanics void RemoveObstacleMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const { if(auto obstacleToRemove = parameters.cb->battleGetObstacleOnPos(parameters.getFirstDestinationHex(), false)) { - ObstaclesRemoved obr; - obr.obstacles.insert(obstacleToRemove->uniqueID); - env->sendAndApply(&obr); + if(canRemove(obstacleToRemove.get(), parameters.spellLvl)) + { + ObstaclesRemoved obr; + obr.obstacles.insert(obstacleToRemove->uniqueID); + env->sendAndApply(&obr); + } + else + { + env->complain("Cant remove this obstacle!"); + } } else env->complain("There's no obstacle to remove!"); } +ESpellCastProblem::ESpellCastProblem RemoveObstacleMechanics::canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const +{ + if(mode == ECastingMode::AFTER_ATTACK_CASTING || mode == ECastingMode::SPELL_LIKE_ATTACK || mode == ECastingMode::MAGIC_MIRROR) + { + logGlobal->warn("Invalid spell cast attempt: spell %s, mode %d", owner->name, mode); //should not even try to do it + return ESpellCastProblem::INVALID; + } + + const int spellLevel = caster->getSpellSchoolLevel(owner); + + for(auto obstacle : cb->battleGetAllObstacles()) + if(canRemove(obstacle.get(), spellLevel)) + return ESpellCastProblem::OK; + + return ESpellCastProblem::NO_APPROPRIATE_TARGET; +} + +ESpellCastProblem::ESpellCastProblem RemoveObstacleMechanics::canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const +{ + if(auto obstacle = cb->battleGetObstacleOnPos(ctx.destination, false)) + if(canRemove(obstacle.get(), ctx.schoolLvl)) + return ESpellCastProblem::OK; + + return ESpellCastProblem::NO_APPROPRIATE_TARGET; +} + +bool RemoveObstacleMechanics::canRemove(const CObstacleInstance * obstacle, const int spellLevel) const +{ + switch (obstacle->obstacleType) + { + case CObstacleInstance::ABSOLUTE_OBSTACLE: //cliff-like obstacles can't be removed + case CObstacleInstance::MOAT: + return false; + case CObstacleInstance::USUAL: + return true; + case CObstacleInstance::FIRE_WALL: + if(spellLevel >= 2) + return true; + break; + case CObstacleInstance::QUICKSAND: + case CObstacleInstance::LAND_MINE: + case CObstacleInstance::FORCE_FIELD: + if(spellLevel >= 3) + return true; + break; + default: + break; + } + return false; +} + +bool RemoveObstacleMechanics::requiresCreatureTarget() const +{ + return false; +} + +///RisingSpellMechanics HealingSpellMechanics::EHealLevel RisingSpellMechanics::getHealLevel(int effectLevel) const { //this may be even distinct class @@ -526,18 +684,19 @@ HealingSpellMechanics::EHealLevel RisingSpellMechanics::getHealLevel(int effectL } ///SacrificeMechanics -ESpellCastProblem::ESpellCastProblem SacrificeMechanics::canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const +ESpellCastProblem::ESpellCastProblem SacrificeMechanics::canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const { + if(mode == ECastingMode::AFTER_ATTACK_CASTING || mode == ECastingMode::SPELL_LIKE_ATTACK || mode == ECastingMode::MAGIC_MIRROR) + { + logGlobal->warn("Invalid spell cast attempt: spell %s, mode %d", owner->name, mode); //should not even try to do it + return ESpellCastProblem::INVALID; + } + // for sacrifice we have to check for 2 targets (one dead to resurrect and one living to destroy) bool targetExists = false; bool targetToSacrificeExists = false; - const CGHeroInstance * caster = nullptr; //todo: use ISpellCaster - - if(cb->battleHasHero(cb->playerToSide(player))) - caster = cb->battleGetFightingHero(cb->playerToSide(player)); - for(const CStack * stack : cb->battleGetAllStacks()) { //using isImmuneBy directly as this mechanics does not have overridden immunity check @@ -545,7 +704,7 @@ ESpellCastProblem::ESpellCastProblem SacrificeMechanics::canBeCast(const CBattle //TODO: check that we really should check immunity for both stacks ESpellCastProblem::ESpellCastProblem res = owner->internalIsImmune(caster, stack); const bool immune = ESpellCastProblem::OK != res && ESpellCastProblem::NOT_DECIDED != res; - const bool casterStack = stack->owner == player; + const bool casterStack = stack->owner == caster->getOwner(); if(!immune && casterStack) { @@ -572,11 +731,7 @@ void SacrificeMechanics::applyBattleEffects(const SpellCastEnvironment * env, co { victim = parameters.destinations[1].stackValue; } - else - { - //todo: remove and report error - victim = parameters.selectedStack; - } + if(nullptr == victim) { env->complain("SacrificeMechanics: No stack to sacrifice"); @@ -592,29 +747,43 @@ void SacrificeMechanics::applyBattleEffects(const SpellCastEnvironment * env, co int SacrificeMechanics::calculateHealedHP(const SpellCastEnvironment* env, const BattleSpellCastParameters& parameters, SpellCastContext& ctx) const { - int res = 0; const CStack * victim = nullptr; if(parameters.destinations.size() == 2) { victim = parameters.destinations[1].stackValue; } - else - { - //todo: remove and report error - victim = parameters.selectedStack; - } + if(nullptr == victim) { env->complain("SacrificeMechanics: No stack to sacrifice"); return 0; } - res = (parameters.effectPower + victim->MaxHealth() + owner->getPower(parameters.effectLevel)) * victim->count; - return res; + return (parameters.effectPower + victim->MaxHealth() + owner->getPower(parameters.effectLevel)) * victim->count; +} + +bool SacrificeMechanics::requiresCreatureTarget() const +{ + return false;//canBeCast do all target existence checks } ///SpecialRisingSpellMechanics +ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const +{ + const CStack * stack = cb->getStackIf([ctx](const CStack * s) + { + const bool ownerMatches = !ctx.ti.smart || s->getOwner() == ctx.caster->getOwner(); + + return ownerMatches && s->isValidTarget(!ctx.ti.onlyAlive) && s->coversPos(ctx.destination); + }); + + if(nullptr == stack) + return ESpellCastProblem::NO_APPROPRIATE_TARGET; + + return ESpellCastProblem::OK; +} + ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const { // following does apply to resurrect and animate dead(?) only @@ -635,13 +804,19 @@ ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::isImmuneByStac } ///SummonMechanics -ESpellCastProblem::ESpellCastProblem SummonMechanics::canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const +ESpellCastProblem::ESpellCastProblem SummonMechanics::canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const { + if(mode == ECastingMode::AFTER_ATTACK_CASTING || mode == ECastingMode::SPELL_LIKE_ATTACK || mode == ECastingMode::MAGIC_MIRROR) + { + logGlobal->warn("Invalid spell cast attempt: spell %s, mode %d", owner->name, mode); //should not even try to do it + return ESpellCastProblem::INVALID; + } + //check if there are summoned elementals of other type - auto otherSummoned = cb->battleGetStacksIf([player, this](const CStack * st) + auto otherSummoned = cb->battleGetStacksIf([caster, this](const CStack * st) { - return (st->owner == player) + return (st->owner == caster->getOwner()) && (vstd::contains(st->state, EBattleStackState::SUMMONED)) && (!vstd::contains(st->state, EBattleStackState::CLONED)) && (st->getCreature()->idNumber != creatureToSummon); @@ -673,26 +848,33 @@ void SummonMechanics::applyBattleEffects(const SpellCastEnvironment * env, const env->complain("Summoning didn't summon any!"); } +bool SummonMechanics::requiresCreatureTarget() const +{ + return false; +} + ///TeleportMechanics void TeleportMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const { //todo: check legal teleport if(parameters.destinations.size() == 2) { - //first destination creature to move - const CStack * target = parameters.destinations[0].stackValue; - if(nullptr == target) - { - env->complain("TeleportMechanics: no stack to teleport"); - return; - } - //second destination hex to move to - const BattleHex destination = parameters.destinations[1].hexValue; + //first destination hex to move to + const BattleHex destination = parameters.destinations[0].hexValue; if(!destination.isValid()) { env->complain("TeleportMechanics: invalid teleport destination"); return; } + + //second destination creature to move + const CStack * target = parameters.destinations[1].stackValue; + if(nullptr == target) + { + env->complain("TeleportMechanics: no stack to teleport"); + return; + } + BattleStackMoved bsm; bsm.distance = -1; bsm.stack = target->ID; @@ -704,16 +886,18 @@ void TeleportMechanics::applyBattleEffects(const SpellCastEnvironment * env, con } else { - //todo: remove and report error - BattleStackMoved bsm; - bsm.distance = -1; - bsm.stack = parameters.selectedStack->ID; - std::vector tiles; - tiles.push_back(parameters.getFirstDestinationHex()); - bsm.tilesToMove = tiles; - bsm.teleporting = true; - env->sendAndApply(&bsm); + env->complain("TeleportMechanics: 2 destinations required."); + return; } } +ESpellCastProblem::ESpellCastProblem TeleportMechanics::canBeCast(const CBattleInfoCallback* cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const +{ + if(mode == ECastingMode::AFTER_ATTACK_CASTING || mode == ECastingMode::SPELL_LIKE_ATTACK || mode == ECastingMode::MAGIC_MIRROR) + { + logGlobal->warn("Invalid spell cast attempt: spell %s, mode %d", owner->name, mode); //should not even try to do it + return ESpellCastProblem::INVALID; + } + return DefaultSpellMechanics::canBeCast(cb, mode, caster); +} diff --git a/lib/spells/BattleSpellMechanics.h b/lib/spells/BattleSpellMechanics.h index 8ac4399a3..62ffe596f 100644 --- a/lib/spells/BattleSpellMechanics.h +++ b/lib/spells/BattleSpellMechanics.h @@ -12,6 +12,9 @@ #include "CDefaultSpellMechanics.h" +class CObstacleInstance; +class SpellCreatedObstacle; + class DLL_LINKAGE HealingSpellMechanics : public DefaultSpellMechanics { public: @@ -41,7 +44,8 @@ class DLL_LINKAGE ChainLightningMechanics : public DefaultSpellMechanics { public: ChainLightningMechanics(CSpell * s): DefaultSpellMechanics(s){}; - std::set getAffectedStacks(SpellTargetingContext & ctx) const override; +protected: + std::vector calculateAffectedStacks(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const override; }; class DLL_LINKAGE CloneMechanics : public DefaultSpellMechanics @@ -59,8 +63,11 @@ public: CureMechanics(CSpell * s): HealingSpellMechanics(s){}; void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const override final; + ESpellCastProblem::ESpellCastProblem isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const override; EHealLevel getHealLevel(int effectLevel) const override final; +private: + static bool dispellSelector(const Bonus * b); }; class DLL_LINKAGE DispellMechanics : public DefaultSpellMechanics @@ -74,11 +81,12 @@ protected: void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; }; -class DLL_LINKAGE EarthquakeMechanics : public DefaultSpellMechanics +class DLL_LINKAGE EarthquakeMechanics : public SpecialSpellMechanics { public: - EarthquakeMechanics(CSpell * s): DefaultSpellMechanics(s){}; - ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const override; + EarthquakeMechanics(CSpell * s): SpecialSpellMechanics(s){}; + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const override; + bool requiresCreatureTarget() const override; protected: void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; }; @@ -90,15 +98,43 @@ public: ESpellCastProblem::ESpellCastProblem isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const override; }; -class DLL_LINKAGE ObstacleMechanics : public DefaultSpellMechanics +class DLL_LINKAGE ObstacleMechanics : public SpecialSpellMechanics { public: - ObstacleMechanics(CSpell * s): DefaultSpellMechanics(s){}; + ObstacleMechanics(CSpell * s): SpecialSpellMechanics(s){}; + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const override; +protected: + static bool isHexAviable(const CBattleInfoCallback * cb, const BattleHex & hex, const bool mustBeClear); + void placeObstacle(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, const BattleHex & pos) const; + virtual void setupObstacle(SpellCreatedObstacle * obstacle) const = 0; +}; +class PatchObstacleMechanics : public ObstacleMechanics +{ +public: + PatchObstacleMechanics(CSpell * s): ObstacleMechanics(s){}; protected: void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; }; +class DLL_LINKAGE LandMineMechanics : public PatchObstacleMechanics +{ +public: + LandMineMechanics(CSpell * s): PatchObstacleMechanics(s){}; + bool requiresCreatureTarget() const override; +protected: + void setupObstacle(SpellCreatedObstacle * obstacle) const override; +}; + +class DLL_LINKAGE QuicksandMechanics : public PatchObstacleMechanics +{ +public: + QuicksandMechanics(CSpell * s): PatchObstacleMechanics(s){}; + bool requiresCreatureTarget() const override; +protected: + void setupObstacle(SpellCreatedObstacle * obstacle) const override; +}; + class DLL_LINKAGE WallMechanics : public ObstacleMechanics { public: @@ -106,12 +142,37 @@ public: std::vector rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes = nullptr) const override; }; -class DLL_LINKAGE RemoveObstacleMechanics : public DefaultSpellMechanics +class DLL_LINKAGE FireWallMechanics : public WallMechanics { public: - RemoveObstacleMechanics(CSpell * s): DefaultSpellMechanics(s){}; + FireWallMechanics(CSpell * s): WallMechanics(s){}; + bool requiresCreatureTarget() const override; protected: void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; + void setupObstacle(SpellCreatedObstacle * obstacle) const override; +}; + +class DLL_LINKAGE ForceFieldMechanics : public WallMechanics +{ +public: + ForceFieldMechanics(CSpell * s): WallMechanics(s){}; + bool requiresCreatureTarget() const override; +protected: + void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; + void setupObstacle(SpellCreatedObstacle * obstacle) const override; +}; + +class DLL_LINKAGE RemoveObstacleMechanics : public SpecialSpellMechanics +{ +public: + RemoveObstacleMechanics(CSpell * s): SpecialSpellMechanics(s){}; + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const override; + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const override; + bool requiresCreatureTarget() const override; +protected: + void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; +private: + bool canRemove(const CObstacleInstance * obstacle, const int spellLevel) const; }; ///all rising spells @@ -119,6 +180,7 @@ class DLL_LINKAGE RisingSpellMechanics : public HealingSpellMechanics { public: RisingSpellMechanics(CSpell * s): HealingSpellMechanics(s){}; + EHealLevel getHealLevel(int effectLevel) const override; }; @@ -127,26 +189,29 @@ class DLL_LINKAGE SacrificeMechanics : public RisingSpellMechanics public: SacrificeMechanics(CSpell * s): RisingSpellMechanics(s){}; - ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const override; + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const override; + bool requiresCreatureTarget() const override; protected: void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; int calculateHealedHP(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; }; -///all rising spells but SACRIFICE +///ANIMATE_DEAD and RESURRECTION class DLL_LINKAGE SpecialRisingSpellMechanics : public RisingSpellMechanics { public: SpecialRisingSpellMechanics(CSpell * s): RisingSpellMechanics(s){}; + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const override; ESpellCastProblem::ESpellCastProblem isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const override; }; -class DLL_LINKAGE SummonMechanics : public DefaultSpellMechanics +class DLL_LINKAGE SummonMechanics : public SpecialSpellMechanics { public: - SummonMechanics(CSpell * s, CreatureID cre): DefaultSpellMechanics(s), creatureToSummon(cre){}; + SummonMechanics(CSpell * s, CreatureID cre): SpecialSpellMechanics(s), creatureToSummon(cre){}; - ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const override; + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const override; + bool requiresCreatureTarget() const override; protected: void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; private: @@ -157,6 +222,8 @@ class DLL_LINKAGE TeleportMechanics: public DefaultSpellMechanics { public: TeleportMechanics(CSpell * s): DefaultSpellMechanics(s){}; + + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const override; protected: void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; }; diff --git a/lib/spells/CDefaultSpellMechanics.cpp b/lib/spells/CDefaultSpellMechanics.cpp index d0906623a..2d48b8cd3 100644 --- a/lib/spells/CDefaultSpellMechanics.cpp +++ b/lib/spells/CDefaultSpellMechanics.cpp @@ -12,7 +12,6 @@ #include "CDefaultSpellMechanics.h" -#include "../NetPacks.h" #include "../BattleState.h" #include "../CGeneralTextHandler.h" @@ -119,6 +118,132 @@ namespace SRSLPraserHelpers } } +SpellCastContext::SpellCastContext(const DefaultSpellMechanics * mechanics_, const SpellCastEnvironment * env_, const BattleSpellCastParameters & parameters_): + mechanics(mechanics_), env(env_), attackedCres(), sc(), si(), parameters(parameters_), otherHero(nullptr), spellCost(0), damageToDisplay(0) +{ + sc.side = parameters.casterSide; + sc.id = mechanics->owner->id; + sc.skill = parameters.spellLvl; + sc.tile = parameters.getFirstDestinationHex(); + sc.castByHero = parameters.mode == ECastingMode::HERO_CASTING; + sc.casterStack = (parameters.casterStack ? parameters.casterStack->ID : -1); + sc.manaGained = 0; + + //check it there is opponent hero + const ui8 otherSide = 1-parameters.casterSide; + + if(parameters.cb->battleHasHero(otherSide)) + otherHero = parameters.cb->battleGetFightingHero(otherSide); + + logGlobal->debugStream() << "Started spell cast. Spell: " << mechanics->owner->name << "; mode:" << parameters.mode; +} + +SpellCastContext::~SpellCastContext() +{ + logGlobal->debugStream() << "Finished spell cast. Spell: " << mechanics->owner->name << "; mode:" << parameters.mode; +} + +void SpellCastContext::addDamageToDisplay(const si32 value) +{ + damageToDisplay += value; +} + +void SpellCastContext::setDamageToDisplay(const si32 value) +{ + damageToDisplay = value; +} + +void SpellCastContext::prepareBattleLog() +{ + bool displayDamage = true; + + mechanics->battleLog(sc.battleLog, parameters, attackedCres, damageToDisplay, displayDamage); + + displayDamage = displayDamage && damageToDisplay > 0; + + if(displayDamage) + { + MetaString line; + + line.addTxt(MetaString::GENERAL_TXT, 376); + line.addReplacement(MetaString::SPELL_NAME, mechanics->owner->id.toEnum()); + line.addReplacement(damageToDisplay); + + sc.battleLog.push_back(line); + } +} + +void SpellCastContext::beforeCast() +{ + //calculate spell cost + if(parameters.mode == ECastingMode::HERO_CASTING) + { + spellCost = parameters.cb->battleGetSpellCost(mechanics->owner, parameters.casterHero); + + if(nullptr != otherHero) //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 == otherHero->tempOwner) + { + vstd::amax(manaChannel, stack->valOfBonuses(Bonus::MANA_CHANNELING)); + } + } + sc.manaGained = (manaChannel * spellCost) / 100; + } + + logGlobal->debugStream() << "spellCost: " << spellCost; + } +} + +void SpellCastContext::afterCast() +{ + for(auto sta : attackedCres) + { + sc.affectedCres.insert(sta->ID); + } + + prepareBattleLog(); + + env->sendAndApply(&sc); + + if(parameters.mode == ECastingMode::HERO_CASTING) + { + //spend mana + SetMana sm; + sm.absolute = false; + + sm.hid = parameters.casterHero->id; + sm.val = -spellCost; + + env->sendAndApply(&sm); + + if(sc.manaGained > 0) + { + assert(otherHero); + + sm.hid = otherHero->id; + sm.val = sc.manaGained; + env->sendAndApply(&sm); + } + } + else if (parameters.mode == ECastingMode::CREATURE_ACTIVE_CASTING || parameters.mode == ECastingMode::ENCHANTER_CASTING) + { + //reduce number of casts remaining + assert(parameters.casterStack); + + BattleSetStackProperty ssp; + ssp.stackID = parameters.casterStack->ID; + ssp.which = BattleSetStackProperty::CASTS; + ssp.val = -1; + ssp.absolute = false; + env->sendAndApply(&ssp); + } + if(!si.stacks.empty()) //after spellcast info shows + env->sendAndApply(&si); +} + ///DefaultSpellMechanics void DefaultSpellMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const { @@ -145,218 +270,27 @@ void DefaultSpellMechanics::applyBattle(BattleInfo * battle, const BattleSpellCa } } -bool DefaultSpellMechanics::adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const +void DefaultSpellMechanics::battleCast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters) const { - if(!owner->isAdventureSpell()) - { - env->complain("Attempt to cast non adventure spell in adventure mode"); - return false; - } - - const CGHeroInstance * caster = parameters.caster; - - if(caster->inTownGarrison) - { - env->complain("Attempt to cast an adventure spell in town garrison"); - return false; - } - - const int cost = caster->getSpellCost(owner); - - if(!caster->canCastThisSpell(owner)) - { - env->complain("Hero cannot cast this spell!"); - return false; - } - - if(caster->mana < cost) - { - env->complain("Hero doesn't have enough spell points to cast this spell!"); - return false; - } - - { - AdvmapSpellCast asc; - asc.caster = caster; - asc.spellID = owner->id; - env->sendAndApply(&asc); - } - - switch(applyAdventureEffects(env, parameters)) - { - case ESpellCastResult::OK: - { - SetMana sm; - sm.hid = caster->id; - sm.absolute = false; - sm.val = -cost; - env->sendAndApply(&sm); - return true; - } - break; - case ESpellCastResult::CANCEL: - return true; - } - return false; -} - -ESpellCastResult DefaultSpellMechanics::applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const -{ - if(owner->hasEffects()) - { - const int schoolLevel = parameters.caster->getSpellSchoolLevel(owner); - - std::vector bonuses; - - owner->getEffects(bonuses, schoolLevel); - - for(Bonus b : bonuses) - { - GiveBonus gb; - gb.id = parameters.caster->id.getNum(); - gb.bonus = b; - env->sendAndApply(&gb); - } - - return ESpellCastResult::OK; - } - else - { - //There is no generic algorithm of adventure cast - env->complain("Unimplemented adventure spell"); - return ESpellCastResult::ERROR; - } -} - -void DefaultSpellMechanics::battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const -{ - logGlobal->debugStream() << "Started spell cast. Spell: "<name<<"; mode:"<complain("No spell-caster provided."); return; } - BattleSpellCast sc; - prepareBattleCast(parameters, sc); - - //check it there is opponent hero - const ui8 otherSide = 1-parameters.casterSide; - const CGHeroInstance * otherHero = nullptr; - if(parameters.cb->battleHasHero(otherSide)) - otherHero = parameters.cb->battleGetFightingHero(otherSide); - int spellCost = 0; - - //calculate spell cost - if(parameters.mode == ECastingMode::HERO_CASTING) - { - spellCost = parameters.cb->battleGetSpellCost(owner, parameters.casterHero); - - if(nullptr != otherHero) //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 == otherHero->tempOwner) - { - vstd::amax(manaChannel, stack->valOfBonuses(Bonus::MANA_CHANNELING)); - } - } - sc.manaGained = (manaChannel * spellCost) / 100; - } - } - logGlobal->debugStream() << "spellCost: " << spellCost; - - //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.getFirstDestinationHex(), parameters.caster); - std::copy(creatures.begin(), creatures.end(), std::back_inserter(attackedCres)); - - logGlobal->debugStream() << "will affect: " << attackedCres.size() << " stacks"; - std::vector reflected;//for magic mirror - //checking if creatures resist - handleResistance(env, attackedCres, sc); - //it is actual spell and can be reflected to single target, no recurrence - const bool tryMagicMirror = owner->isNegative() && owner->level && owner->getLevelInfo(0).range == "0"; - if(tryMagicMirror) - { - for(auto s : attackedCres) - { - const int mirrorChance = (s)->valOfBonuses(Bonus::MAGIC_MIRROR); - if(env->getRandomGenerator().nextInt(99) < mirrorChance) - reflected.push_back(s); - } - vstd::erase_if(attackedCres, [&reflected](const CStack * s) - { - return vstd::contains(reflected, s); - }); + cast(env, parameters, reflected); - for(auto s : reflected) - { - BattleSpellCast::CustomEffect effect; - effect.effect = 3; - effect.stack = s->ID; - sc.customEffects.push_back(effect); - } - } - - for(auto cre : attackedCres) - { - sc.affectedCres.insert(cre->ID); - } - - StacksInjured si; - SpellCastContext ctx(attackedCres, sc, si); - applyBattleEffects(env, parameters, ctx); - - env->sendAndApply(&sc); - - //spend mana - if(parameters.mode == ECastingMode::HERO_CASTING) - { - SetMana sm; - sm.absolute = false; - - sm.hid = parameters.casterHero->id; - sm.val = -spellCost; - - env->sendAndApply(&sm); - - if(sc.manaGained > 0) - { - assert(otherHero); - - sm.hid = otherHero->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); - } - logGlobal->debugStream() << "Finished spell cast. Spell: "<name<<"; mode:"<error("Magic mirror recurrence!"); + return; + } + TStacks mirrorTargets = parameters.cb->battleGetStacksIf([this, parameters](const CStack * battleStack) { //Get all enemy stacks. Magic mirror can reflect to immune creature (with no effect) @@ -367,48 +301,68 @@ void DefaultSpellMechanics::battleCast(const SpellCastEnvironment * env, BattleS { int targetHex = (*RandomGeneratorUtil::nextItem(mirrorTargets, env->getRandomGenerator()))->position; - BattleSpellCastParameters mirrorParameters(parameters.cb, attackedCre, owner); - mirrorParameters.spellLvl = 0; + BattleSpellCastParameters mirrorParameters(parameters, attackedCre); mirrorParameters.aimToHex(targetHex); - mirrorParameters.mode = ECastingMode::MAGIC_MIRROR; - mirrorParameters.selectedStack = nullptr; - mirrorParameters.spellLvl = parameters.spellLvl; - mirrorParameters.effectLevel = parameters.effectLevel; - mirrorParameters.effectPower = parameters.effectPower; - mirrorParameters.effectValue = parameters.effectValue; - mirrorParameters.enchantPower = parameters.enchantPower; - castMagicMirror(env, mirrorParameters); + mirrorParameters.cast(env); } } } -void DefaultSpellMechanics::battleLogSingleTarget(std::vector & logLines, const BattleSpellCast * packet, - const std::string & casterName, const CStack * attackedStack, bool & displayDamage) const +void DefaultSpellMechanics::cast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, std::vector & reflected) const { - const std::string attackedName = attackedStack->getName(); - const std::string attackedNameSing = attackedStack->getCreature()->nameSing; - const std::string attackedNamePl = attackedStack->getCreature()->namePl; + SpellCastContext ctx(this, env, parameters); - auto getPluralFormat = [attackedStack](const int baseTextID) -> boost::format + ctx.beforeCast(); + + ctx.attackedCres = owner->getAffectedStacks(parameters.cb, parameters.mode, parameters.caster, parameters.spellLvl, parameters.getFirstDestinationHex()); + + logGlobal->debugStream() << "will affect " << ctx.attackedCres.size() << " stacks"; + + handleResistance(env, ctx); + + if(parameters.mode != ECastingMode::MAGIC_MIRROR) + handleMagicMirror(env, ctx, reflected); + + applyBattleEffects(env, parameters, ctx); + + ctx.afterCast(); +} + +void DefaultSpellMechanics::battleLog(std::vector & logLines, const BattleSpellCastParameters & parameters, + const std::vector & attacked, const si32 damageToDisplay, bool & displayDamage) const +{ + if(attacked.size() != 1) { - return boost::format(VLC->generaltexth->allTexts[(attackedStack->count > 1 ? baseTextID + 1 : baseTextID)]); + displayDamage = true; + battleLogDefault(logLines, parameters, attacked); + return; + } + + auto attackedStack = attacked.at(0); + + auto getPluralFormat = [attackedStack](const int baseTextID) -> si32 + { + return attackedStack->count > 1 ? baseTextID + 1 : baseTextID; }; - auto logSimple = [&logLines, getPluralFormat, attackedName](const int baseTextID) + auto logSimple = [attackedStack, &logLines, getPluralFormat](const int baseTextID) { - boost::format fmt = getPluralFormat(baseTextID); - fmt % attackedName; - logLines.push_back(fmt.str()); + MetaString line; + line.addTxt(MetaString::GENERAL_TXT, getPluralFormat(baseTextID)); + line.addReplacement(*attackedStack); + logLines.push_back(line); }; - auto logPlural = [&logLines, attackedNamePl](const int baseTextID) + auto logPlural = [attackedStack, &logLines, getPluralFormat](const int baseTextID) { - boost::format fmt(VLC->generaltexth->allTexts[baseTextID]); - fmt % attackedNamePl; - logLines.push_back(fmt.str()); + MetaString line; + line.addTxt(MetaString::GENERAL_TXT, baseTextID); + line.addReplacement(MetaString::CRE_PL_NAMES, attackedStack->getCreature()->idNumber.num); + logLines.push_back(line); }; displayDamage = false; //in most following cases damage info text is custom + switch(owner->id) { case SpellID::STONE_GAZE: @@ -428,74 +382,78 @@ void DefaultSpellMechanics::battleLogSingleTarget(std::vector & log break; case SpellID::AGE: { - boost::format text = getPluralFormat(551); - text % attackedName; //The %s shrivel with age, and lose %d hit points." + MetaString line; + line.addTxt(MetaString::GENERAL_TXT, getPluralFormat(551)); + line.addReplacement(MetaString::CRE_PL_NAMES, attackedStack->getCreature()->idNumber.num); + + //todo: display effect from only this cast TBonusListPtr bl = attackedStack->getBonuses(Selector::type(Bonus::STACK_HEALTH)); const int fullHP = bl->totalValue(); bl->remove_if(Selector::source(Bonus::SPELL_EFFECT, SpellID::AGE)); - text % (fullHP - bl->totalValue()); - logLines.push_back(text.str()); + line.addReplacement(fullHP - bl->totalValue()); + logLines.push_back(line); } break; case SpellID::THUNDERBOLT: { logPlural(367); + MetaString line; + //todo: handle newlines in metastring std::string text = VLC->generaltexth->allTexts[343].substr(1, VLC->generaltexth->allTexts[343].size() - 1); //Does %d points of damage. - boost::algorithm::replace_first(text, "%d", boost::lexical_cast(packet->dmgToDisplay)); //no more text afterwards - logLines.push_back(text); + line << text; + line.addReplacement(damageToDisplay); //no more text afterwards + logLines.push_back(line); } break; case SpellID::DISPEL_HELPFUL_SPELLS: logPlural(555); break; case SpellID::DEATH_STARE: - if (packet->dmgToDisplay > 0) + if (damageToDisplay > 0) { - std::string text; - if (packet->dmgToDisplay > 1) + MetaString line; + if (damageToDisplay > 1) { - text = VLC->generaltexth->allTexts[119]; //%d %s die under the terrible gaze of the %s. - boost::algorithm::replace_first(text, "%d", boost::lexical_cast(packet->dmgToDisplay)); - boost::algorithm::replace_first(text, "%s", attackedNamePl); + line.addTxt(MetaString::GENERAL_TXT, 119); //%d %s die under the terrible gaze of the %s. + line.addReplacement(damageToDisplay); + line.addReplacement(MetaString::CRE_PL_NAMES, attackedStack->getCreature()->idNumber.num); } else { - text = VLC->generaltexth->allTexts[118]; //One %s dies under the terrible gaze of the %s. - boost::algorithm::replace_first(text, "%s", attackedNameSing); + line.addTxt(MetaString::GENERAL_TXT, 118); //One %s dies under the terrible gaze of the %s. + line.addReplacement(MetaString::CRE_SING_NAMES, attackedStack->getCreature()->idNumber.num); } - boost::algorithm::replace_first(text, "%s", casterName); //casting stack - logLines.push_back(text); + parameters.caster->getCasterName(line); + logLines.push_back(line); } break; default: - { - boost::format text(VLC->generaltexth->allTexts[565]); //The %s casts %s - text % casterName % owner->name; - displayDamage = true; - logLines.push_back(text.str()); - } + displayDamage = true; + battleLogDefault(logLines, parameters, attacked); break; } } +void DefaultSpellMechanics::battleLogDefault(std::vector & logLines, const BattleSpellCastParameters & parameters, const std::vector & attacked) const +{ + MetaString line; + parameters.caster->getCastDescription(owner, attacked, line); + logLines.push_back(line); +} + void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const { //applying effects if(owner->isOffensiveSpell()) { - int spellDamage = parameters.effectValue; - + const int rawDamage = parameters.getEffectValue(); int chainLightningModifier = 0; for(auto & attackedCre : ctx.attackedCres) { BattleStackAttacked bsa; - if(spellDamage != 0) - bsa.damageAmount = owner->adjustRawDamage(parameters.caster, attackedCre, spellDamage) >> chainLightningModifier; - else - bsa.damageAmount = owner->calculateDamage(parameters.caster, attackedCre, parameters.effectLevel, parameters.effectPower) >> chainLightningModifier; - - ctx.sc.dmgToDisplay += bsa.damageAmount; + bsa.damageAmount = owner->adjustRawDamage(parameters.caster, attackedCre, rawDamage) >> chainLightningModifier; + ctx.addDamageToDisplay(bsa.damageAmount); bsa.stackAttacked = (attackedCre)->ID; if(parameters.mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast @@ -598,7 +556,6 @@ void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, if(!sse.stacks.empty()) env->sendAndApply(&sse); - } } @@ -609,7 +566,7 @@ std::vector DefaultSpellMechanics::rangeInHexes(BattleHex centralHex, 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) + if(rng.size() >= 2 && rng[0] != 'X') //there is at least one hex in range (+artificial comma) { std::string number1, number2; int beg, end; @@ -668,85 +625,94 @@ std::vector DefaultSpellMechanics::rangeInHexes(BattleHex centralHex, return ret; } -std::set DefaultSpellMechanics::getAffectedStacks(SpellTargetingContext & ctx) const +std::vector DefaultSpellMechanics::getAffectedStacks(const CBattleInfoCallback * cb, SpellTargetingContext & ctx) const +{ + std::vector attackedCres = calculateAffectedStacks(cb, ctx); + handleImmunities(cb, ctx, attackedCres); + return attackedCres; +} + +std::vector DefaultSpellMechanics::calculateAffectedStacks(const CBattleInfoCallback* cb, const 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 ui8 attackerSide = cb->playerToSide(ctx.caster->getOwner()) == 1; + auto attackedHexes = rangeInHexes(ctx.destination, ctx.schoolLvl, attackerSide); - const CSpell::TargetInfo ti(owner, ctx.schoolLvl, ctx.mode); + //hackfix for banned creature massive spells + if(!ctx.ti.massive && owner->getLevelInfo(ctx.schoolLvl).range == "X") + attackedHexes.push_back(ctx.destination); - //TODO: more generic solution for mass spells - if(owner->getLevelInfo(ctx.schoolLvl).range.size() > 1) //custom many-hex range + auto mainFilter = [=](const CStack * s) { - for(BattleHex hex : attackedHexes) + const bool positiveToAlly = owner->isPositive() && s->owner == ctx.caster->getOwner(); + const bool negativeToEnemy = owner->isNegative() && s->owner != ctx.caster->getOwner(); + const bool validTarget = s->isValidTarget(!ctx.ti.onlyAlive); //todo: this should be handled by spell class + const bool positivenessFlag = !ctx.ti.smart || owner->isNeutral() || positiveToAlly || negativeToEnemy; + + return positivenessFlag && validTarget; + }; + + if(ctx.ti.type == CSpell::CREATURE && attackedHexes.size() == 1) + { + //for single target spells we must select one target. Alive stack is preferred (issue #1763) + + auto predicate = [&](const CStack * s) { - 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; + return s->coversPos(attackedHexes.at(0)) && mainFilter(s); }; - TStacks stacks = ctx.cb->battleGetStacksIf(predicate); + TStacks stacks = cb->battleGetStacksIf(predicate); - if(ti.massive) + for(auto stack : stacks) { - //for massive spells add all targets - for (auto stack : stacks) + if(stack->alive()) + { 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; - } + break; } + } - if(attackedCres.empty() && !stacks.empty()) - { - attackedCres.insert(stacks.front()); - } + if(attackedCres.empty() && !stacks.empty()) + { + attackedCres.insert(stacks.front()); } } + else if(ctx.ti.massive) + { + TStacks stacks = cb->battleGetStacksIf(mainFilter); + for (auto stack : stacks) + attackedCres.insert(stack); + } else //custom range from attackedHexes { for(BattleHex hex : attackedHexes) { - if(const CStack * st = ctx.cb->battleGetStackByPos(hex, ti.onlyAlive)) - attackedCres.insert(st); + if(const CStack * st = cb->battleGetStackByPos(hex, ctx.ti.onlyAlive)) + if(mainFilter(st)) + attackedCres.insert(st);; } } - return attackedCres; + std::vector res; + std::copy(attackedCres.begin(), attackedCres.end(), std::back_inserter(res)); + + return res; } -ESpellCastProblem::ESpellCastProblem DefaultSpellMechanics::canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const +ESpellCastProblem::ESpellCastProblem DefaultSpellMechanics::canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const { //no problems by default, this method is for spell-specific problems return ESpellCastProblem::OK; } +ESpellCastProblem::ESpellCastProblem DefaultSpellMechanics::canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const +{ + //no problems by default, this method is for spell-specific problems + //common problems handled by CSpell + return ESpellCastProblem::OK; +} + ESpellCastProblem::ESpellCastProblem DefaultSpellMechanics::isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const { //by default use general algorithm @@ -774,58 +740,56 @@ void DefaultSpellMechanics::doDispell(BattleInfo * battle, const BattleSpellCast } } -void DefaultSpellMechanics::castMagicMirror(const SpellCastEnvironment* env, BattleSpellCastParameters& parameters) const +void DefaultSpellMechanics::handleImmunities(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx, std::vector & stacks) const { - logGlobal->debugStream() << "Started spell cast. Spell: "<name<<"; mode: MAGIC_MIRROR"; - if(parameters.mode != ECastingMode::MAGIC_MIRROR) + //now handle immunities + auto predicate = [&, this](const CStack * s)->bool { - env->complain("MagicMirror: invalid mode"); - return; - } - BattleHex destination = parameters.getFirstDestinationHex(); - if(!destination.isValid()) - { - env->complain("MagicMirror: invalid destination"); - return; - } + bool hitDirectly = ctx.ti.alwaysHitDirectly && s->coversPos(ctx.destination); + bool notImmune = (ESpellCastProblem::OK == owner->isImmuneByStack(ctx.caster, s)); - BattleSpellCast sc; - prepareBattleCast(parameters, sc); - - //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, destination, parameters.caster); - std::copy(creatures.begin(), creatures.end(), std::back_inserter(attackedCres)); - - logGlobal->debugStream() << "will affect: " << attackedCres.size() << " stacks"; - - handleResistance(env, attackedCres, sc); - - for(auto cre : attackedCres) - { - sc.affectedCres.insert(cre->ID); - } - - StacksInjured si; - SpellCastContext ctx(attackedCres, sc, si); - applyBattleEffects(env, parameters, ctx); - - env->sendAndApply(&sc); - if(!si.stacks.empty()) //after spellcast info shows - env->sendAndApply(&si); - logGlobal->debugStream() << "Finished spell cast. Spell: "<name<<"; mode: MAGIC_MIRROR"; + return !(hitDirectly || notImmune); + }; + vstd::erase_if(stacks, predicate); } -void DefaultSpellMechanics::handleResistance(const SpellCastEnvironment * env, std::vector& attackedCres, BattleSpellCast& sc) const +void DefaultSpellMechanics::handleMagicMirror(const SpellCastEnvironment * env, SpellCastContext & ctx, std::vector & reflected) const +{ + //reflection is applied only to negative spells + //if it is actual spell and can be reflected to single target, no recurrence + const bool tryMagicMirror = owner->isNegative() && owner->level && owner->getLevelInfo(0).range == "0"; + if(tryMagicMirror) + { + for(auto s : ctx.attackedCres) + { + const int mirrorChance = (s)->valOfBonuses(Bonus::MAGIC_MIRROR); + if(env->getRandomGenerator().nextInt(99) < mirrorChance) + reflected.push_back(s); + } + + vstd::erase_if(ctx.attackedCres, [&reflected](const CStack * s) + { + return vstd::contains(reflected, s); + }); + + for(auto s : reflected) + { + BattleSpellCast::CustomEffect effect; + effect.effect = 3; + effect.stack = s->ID; + ctx.sc.customEffects.push_back(effect); + } + } +} + +void DefaultSpellMechanics::handleResistance(const SpellCastEnvironment * env, SpellCastContext & ctx) const { //checking if creatures resist - //resistance/reflection is applied only to negative spells + //resistance is applied only to negative spells if(owner->isNegative()) { std::vector resisted; - for(auto s : attackedCres) + for(auto s : ctx.attackedCres) { //magic resistance const int prob = std::min((s)->magicResistance(), 100); //probability of resistance in % @@ -836,7 +800,7 @@ void DefaultSpellMechanics::handleResistance(const SpellCastEnvironment * env, s } } - vstd::erase_if(attackedCres, [&resisted](const CStack * s) + vstd::erase_if(ctx.attackedCres, [&resisted](const CStack * s) { return vstd::contains(resisted, s); }); @@ -846,19 +810,19 @@ void DefaultSpellMechanics::handleResistance(const SpellCastEnvironment * env, s BattleSpellCast::CustomEffect effect; effect.effect = 78; effect.stack = s->ID; - sc.customEffects.push_back(effect); + ctx.sc.customEffects.push_back(effect); } } } -void DefaultSpellMechanics::prepareBattleCast(const BattleSpellCastParameters& parameters, BattleSpellCast& sc) const +bool DefaultSpellMechanics::requiresCreatureTarget() const { - sc.side = parameters.casterSide; - sc.id = owner->id; - sc.skill = parameters.spellLvl; - sc.tile = parameters.getFirstDestinationHex(); - sc.dmgToDisplay = 0; - sc.castByHero = parameters.mode == ECastingMode::HERO_CASTING; - sc.casterStack = (parameters.casterStack ? parameters.casterStack->ID : -1); - sc.manaGained = 0; + //most spells affects creatures somehow regardless of Target Type + //for few exceptions see overrides + return true; +} + +std::vector SpecialSpellMechanics::calculateAffectedStacks(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const +{ + return std::vector(); } diff --git a/lib/spells/CDefaultSpellMechanics.h b/lib/spells/CDefaultSpellMechanics.h index 910d25fd9..38914ff17 100644 --- a/lib/spells/CDefaultSpellMechanics.h +++ b/lib/spells/CDefaultSpellMechanics.h @@ -11,54 +11,83 @@ #pragma once #include "ISpellMechanics.h" +#include "../NetPacks.h" -struct StacksInjured; +class DefaultSpellMechanics; -struct SpellCastContext +class DLL_LINKAGE SpellCastContext { - SpellCastContext(std::vector & attackedCres, BattleSpellCast & sc, StacksInjured & si): - attackedCres(attackedCres), sc(sc), si(si) - { - }; - std::vector & attackedCres; - BattleSpellCast & sc; - StacksInjured & si; -}; - -enum class ESpellCastResult -{ - OK, - CANCEL,//cast failed but it is not an error - ERROR//internal error occurred +public: + const DefaultSpellMechanics * mechanics; + const SpellCastEnvironment * env; + std::vector attackedCres;//must be vector, as in Chain Lightning order matters + BattleSpellCast sc;//todo: make private + StacksInjured si; + const BattleSpellCastParameters & parameters; + + SpellCastContext(const DefaultSpellMechanics * mechanics_, const SpellCastEnvironment * env_, const BattleSpellCastParameters & parameters_); + virtual ~SpellCastContext(); + + void addDamageToDisplay(const si32 value); + void setDamageToDisplay(const si32 value); + + void beforeCast(); + void afterCast(); +private: + const CGHeroInstance * otherHero; + int spellCost; + si32 damageToDisplay; + + void prepareBattleLog(); }; +///all combat spells class DLL_LINKAGE 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; + std::vector getAffectedStacks(const CBattleInfoCallback * cb, SpellTargetingContext & ctx) const override final; - ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const override; + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const override; + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const override; ESpellCastProblem::ESpellCastProblem isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const override; virtual void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const override; - bool adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override final; - void battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const override final; - void battleLogSingleTarget(std::vector & logLines, const BattleSpellCast * packet, - const std::string & casterName, const CStack * attackedStack, bool & displayDamage) const override; + void battleCast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters) const override final; + + void battleLog(std::vector & logLines, const BattleSpellCastParameters & parameters, + const std::vector & attacked, const si32 damageToDisplay, bool & displayDamage) const; + + void battleLogDefault(std::vector & logLines, const BattleSpellCastParameters & parameters, + const std::vector & attacked) const; + + bool requiresCreatureTarget() const override; protected: virtual void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const; - ///actual adventure cast implementation - virtual ESpellCastResult applyAdventureEffects(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const; + virtual std::vector calculateAffectedStacks(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const; +protected: void doDispell(BattleInfo * battle, const BattleSpellCast * packet, const CSelector & selector) const; private: - void castMagicMirror(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const; - void handleResistance(const SpellCastEnvironment * env, std::vector & attackedCres, BattleSpellCast & sc) const; - void prepareBattleCast(const BattleSpellCastParameters & parameters, BattleSpellCast & sc) const; + void cast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, std::vector & reflected) const; + + void handleImmunities(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx, std::vector & stacks) const; + void handleMagicMirror(const SpellCastEnvironment * env, SpellCastContext & ctx, std::vector & reflected) const; + void handleResistance(const SpellCastEnvironment * env, SpellCastContext & ctx) const; + + friend class SpellCastContext; +}; + +///not affecting creatures directly +class DLL_LINKAGE SpecialSpellMechanics : public DefaultSpellMechanics +{ +public: + SpecialSpellMechanics(CSpell * s): DefaultSpellMechanics(s){}; +protected: + std::vector calculateAffectedStacks(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const override; }; diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index d847a676a..c33d523fd 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -91,14 +91,15 @@ CSpell::CSpell(): defaultProbability(0), isRising(false), isDamage(false), isOffensive(false), targetType(ETargetType::NO_TARGET), - mechanics(nullptr) + mechanics(), + adventureMechanics() { levels.resize(GameConstants::SPELL_SCHOOL_LEVELS); } CSpell::~CSpell() { - delete mechanics; + } void CSpell::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const @@ -110,10 +111,15 @@ bool CSpell::adventureCast(const SpellCastEnvironment * env, AdventureSpellCastP { assert(env); - return mechanics->adventureCast(env, parameters); + if(!adventureMechanics.get()) + { + env->complain("Invalid adventure spell cast attempt!"); + return false; + } + return adventureMechanics->adventureCast(env, parameters); } -void CSpell::battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const +void CSpell::battleCast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters) const { assert(env); if(parameters.destinations.size()<1) @@ -143,9 +149,61 @@ ui32 CSpell::calculateDamage(const ISpellCaster * caster, const CStack * affecte return adjustRawDamage(caster, affectedCreature, calculateRawEffectValue(spellSchoolLevel, usedSpellPower)); } -ESpellCastProblem::ESpellCastProblem CSpell::canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const +ESpellCastProblem::ESpellCastProblem CSpell::canBeCast(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, const ISpellCaster * caster) const { - return mechanics->canBeCast(cb, player); + const ESpellCastProblem::ESpellCastProblem generalProblem = mechanics->canBeCast(cb, mode, caster); + + if(generalProblem != ESpellCastProblem::OK) + return generalProblem; + + //check for creature target existence + if(mechanics->requiresCreatureTarget()) + { + switch(mode) + { + case ECastingMode::HERO_CASTING: + case ECastingMode::CREATURE_ACTIVE_CASTING: + case ECastingMode::ENCHANTER_CASTING: + case ECastingMode::PASSIVE_CASTING: + { + TargetInfo tinfo(this, caster->getSpellSchoolLevel(this), mode); + + bool targetExists = false; + + for(const CStack * stack : cb->battleGetAllStacks()) + { + bool immune = !(stack->isValidTarget(!tinfo.onlyAlive) && ESpellCastProblem::OK == isImmuneByStack(caster, stack)); + bool casterStack = stack->owner == caster->getOwner(); + + if(!immune) + { + switch (positiveness) + { + case CSpell::POSITIVE: + if(casterStack || !tinfo.smart) + targetExists = true; + break; + case CSpell::NEUTRAL: + targetExists = true; + break; + case CSpell::NEGATIVE: + if(!casterStack || !tinfo.smart) + targetExists = true; + break; + } + } + if(targetExists) + break; + } + if(!targetExists) + { + return ESpellCastProblem::NO_APPROPRIATE_TARGET; + } + } + break; + } + } + return ESpellCastProblem::OK; } std::vector CSpell::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const @@ -153,23 +211,10 @@ std::vector CSpell::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, return mechanics->rangeInHexes(centralHex,schoolLvl,side,outDroppedHexes); } -std::set CSpell::getAffectedStacks(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, PlayerColor casterColor, int spellLvl, BattleHex destination, const ISpellCaster * caster) const +std::vector CSpell::getAffectedStacks(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, const ISpellCaster * caster, int spellLvl, BattleHex destination) 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; + SpellTargetingContext ctx(this, mode, caster, spellLvl, destination); + return mechanics->getAffectedStacks(cb, ctx);; } CSpell::ETargetType CSpell::getTargetType() const @@ -222,11 +267,6 @@ bool CSpell::isNeutral() const return positiveness == NEUTRAL; } -bool CSpell::isHealingSpell() const -{ - return isRisingSpell() || (id == SpellID::CURE); -} - bool CSpell::isRisingSpell() const { return isRising; @@ -305,6 +345,35 @@ void CSpell::getEffects(std::vector & lst, const int level) const } } +ESpellCastProblem::ESpellCastProblem CSpell::canBeCastAt(const CBattleInfoCallback * cb, const ISpellCaster * caster, ECastingMode::ECastingMode mode, BattleHex destination) const +{ + SpellTargetingContext ctx(this, mode, caster, caster->getSpellSchoolLevel(this), destination); + + ESpellCastProblem::ESpellCastProblem specific = mechanics->canBeCast(cb, ctx); + + if(specific != ESpellCastProblem::OK) + return specific; + + //todo: this should be moved to mechanics + //rising spells handled by mechanics + if(ctx.ti.onlyAlive && getTargetType() == CSpell::CREATURE) + { + const CStack * aliveStack = cb->getStackIf([destination](const CStack * s) + { + return s->isValidTarget(false) && s->coversPos(destination); + }); + + if(!aliveStack) + return ESpellCastProblem::NO_APPROPRIATE_TARGET; + if(ctx.ti.smart && isNegative() && aliveStack->owner == caster->getOwner()) + return ESpellCastProblem::NO_APPROPRIATE_TARGET; + if(ctx.ti.smart && isPositive() && aliveStack->owner != caster->getOwner()) + return ESpellCastProblem::NO_APPROPRIATE_TARGET; + } + + return isImmuneAt(cb, caster, mode, destination); +} + ESpellCastProblem::ESpellCastProblem CSpell::isImmuneAt(const CBattleInfoCallback * cb, const ISpellCaster * caster, ECastingMode::ECastingMode mode, BattleHex destination) const { // Get all stacks at destination hex. only alive if not rising spell @@ -517,59 +586,6 @@ ESpellCastProblem::ESpellCastProblem CSpell::isImmuneByStack(const ISpellCaster return ESpellCastProblem::OK; } -void CSpell::prepareBattleLog(const CBattleInfoCallback * cb, const BattleSpellCast * packet, std::vector & logLines) const -{ - bool displayDamage = true; - - std::string casterName("Something"); //todo: localize - - if(packet->castByHero) - casterName = cb->battleGetHeroInfo(packet->side).name; - - { - const auto casterStackID = packet->casterStack; - - if(casterStackID > 0) - { - const CStack * casterStack = cb->battleGetStackByID(casterStackID); - if(casterStack != nullptr) - casterName = casterStack->type->namePl; - } - } - - if(packet->affectedCres.size() == 1) - { - const CStack * attackedStack = cb->battleGetStackByID(*packet->affectedCres.begin(), false); - - const std::string attackedNamePl = attackedStack->getCreature()->namePl; - - if(packet->castByHero) - { - const std::string fmt = VLC->generaltexth->allTexts[195]; - logLines.push_back(boost::to_string(boost::format(fmt) % casterName % this->name % attackedNamePl)); - } - else - { - mechanics->battleLogSingleTarget(logLines, packet, casterName, attackedStack, displayDamage); - } - } - else - { - boost::format text(VLC->generaltexth->allTexts[196]); - text % casterName % this->name; - logLines.push_back(text.str()); - } - - - if(packet->dmgToDisplay > 0 && displayDamage) - { - boost::format dmgInfo(VLC->generaltexth->allTexts[376]); - dmgInfo % this->name % packet->dmgToDisplay; - logLines.push_back(dmgInfo.str()); - } -} - - void CSpell::setIsOffensive(const bool val) { isOffensive = val; @@ -598,13 +614,8 @@ void CSpell::setup() void CSpell::setupMechanics() { - if(nullptr != mechanics) - { - logGlobal->errorStream() << "Spell " << this->name << ": mechanics already set"; - delete mechanics; - } - mechanics = ISpellMechanics::createMechanics(this); + adventureMechanics = IAdventureSpellMechanics::createMechanics(this); } ///CSpell::AnimationInfo @@ -661,6 +672,10 @@ CSpell::TargetInfo::TargetInfo(const CSpell * spell, const int level, ECastingMo { alwaysHitDirectly = true; } + else if(mode == ECastingMode::CREATURE_ACTIVE_CASTING) + { + massive = false;//FIXME: find better solution for Commander spells + } } void CSpell::TargetInfo::init(const CSpell * spell, const int level) diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index 01d798f9c..aeb7c2c1a 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -20,6 +20,7 @@ class CGObjectInstance; class CSpell; class ISpellMechanics; +class IAdventureSpellMechanics; class CLegacyConfigParser; class CGHeroInstance; class CStack; @@ -204,7 +205,6 @@ public: bool isNeutral() const; bool isDamageSpell() const; - bool isHealingSpell() const; bool isRisingSpell() const; bool isOffensiveSpell() const; @@ -218,7 +218,7 @@ public: ui32 calculateDamage(const ISpellCaster * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const; ///selects from allStacks actually affected stacks - std::set getAffectedStacks(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, PlayerColor casterColor, int spellLvl, BattleHex destination, const ISpellCaster * caster) const; + std::vector getAffectedStacks(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, const ISpellCaster * caster, int spellLvl, BattleHex destination) const; si32 getCost(const int skillLevel) const; @@ -266,11 +266,11 @@ public: public: ///internal interface (for callbacks) - ///Checks general but spell-specific problems for all casting modes. Use only during battle. - ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const; + ///Checks general but spell-specific problems. Use only during battle. + ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, const ISpellCaster * caster) 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 ISpellCaster * caster, ECastingMode::ECastingMode mode, BattleHex destination) const; + ESpellCastProblem::ESpellCastProblem canBeCastAt(const CBattleInfoCallback * cb, const ISpellCaster * caster, ECastingMode::ECastingMode mode, BattleHex destination) 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 ISpellCaster * caster, const CStack * obj) const; @@ -279,7 +279,7 @@ public: ///May be executed on client side by (future) non-cheat-proof scripts. bool adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const; - void battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const; + void battleCast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters) const; public: ///Client-server logic. Has direct write access to GameState. @@ -287,10 +287,6 @@ public: ///implementation of BattleSpellCast applying void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const; - -public://Client logic. - void prepareBattleLog(const CBattleInfoCallback * cb, const BattleSpellCast * packet, std::vector & logLines) const; - public://internal, for use only by Mechanics classes ///applies caster`s secondary skills and affectedCreature`s to raw damage int adjustRawDamage(const ISpellCaster * caster, const CStack * affectedCreature, int rawDamage) const; @@ -300,6 +296,10 @@ public://internal, for use only by Mechanics classes ESpellCastProblem::ESpellCastProblem internalIsImmune(const ISpellCaster * caster, const CStack *obj) const; private: + + ///checks for creature immunity *at given hex*. + ESpellCastProblem::ESpellCastProblem isImmuneAt(const CBattleInfoCallback * cb, const ISpellCaster * caster, ECastingMode::ECastingMode mode, BattleHex destination) const; + void setIsOffensive(const bool val); void setIsRising(const bool val); @@ -335,7 +335,8 @@ private: std::vector levels; - ISpellMechanics * mechanics;//(!) do not serialize + std::unique_ptr mechanics;//(!) do not serialize + std::unique_ptr adventureMechanics;//(!) do not serialize }; bool DLL_LINKAGE isInScreenRange(const int3 ¢er, const int3 &pos); //for spells like Dimension Door diff --git a/lib/spells/CreatureSpellMechanics.cpp b/lib/spells/CreatureSpellMechanics.cpp index 0879a0c2a..7c545c7be 100644 --- a/lib/spells/CreatureSpellMechanics.cpp +++ b/lib/spells/CreatureSpellMechanics.cpp @@ -20,9 +20,9 @@ void AcidBreathDamageMechanics::applyBattleEffects(const SpellCastEnvironment * { //todo: this should be effectValue //calculating dmg to display - ctx.sc.dmgToDisplay = parameters.effectPower; + ctx.setDamageToDisplay(parameters.effectPower); - for(auto & attackedCre : ctx.attackedCres) //no immunities + for(auto & attackedCre : ctx.attackedCres) { BattleStackAttacked bsa; bsa.flags |= BattleStackAttacked::SPELL_EFFECT; @@ -35,13 +35,35 @@ void AcidBreathDamageMechanics::applyBattleEffects(const SpellCastEnvironment * } } +ESpellCastProblem::ESpellCastProblem AcidBreathDamageMechanics::isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const +{ + //just in case + if(!obj->alive()) + return ESpellCastProblem::WRONG_SPELL_TARGET; + + //there should be no immunities by design + //but make it a bit configurable + //ignore all immunities, except specific absolute immunity + { + //SPELL_IMMUNITY absolute case + std::stringstream cachingStr; + cachingStr << "type_" << Bonus::SPELL_IMMUNITY << "subtype_" << owner->id.toEnum() << "addInfo_1"; + if(obj->hasBonus(Selector::typeSubtypeInfo(Bonus::SPELL_IMMUNITY, owner->id.toEnum(), 1), cachingStr.str())) + return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; + } + return ESpellCastProblem::OK; +} + ///DeathStareMechanics void DeathStareMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const { //calculating dmg to display - ctx.sc.dmgToDisplay = parameters.effectPower; + si32 damageToDisplay = parameters.effectPower; + if(!ctx.attackedCres.empty()) - vstd::amin(ctx.sc.dmgToDisplay, (*ctx.attackedCres.begin())->count); //stack is already reduced after attack + vstd::amin(damageToDisplay, (*ctx.attackedCres.begin())->count); //stack is already reduced after attack + + ctx.setDamageToDisplay(damageToDisplay); for(auto & attackedCre : ctx.attackedCres) { diff --git a/lib/spells/CreatureSpellMechanics.h b/lib/spells/CreatureSpellMechanics.h index 7cd75af03..38affa209 100644 --- a/lib/spells/CreatureSpellMechanics.h +++ b/lib/spells/CreatureSpellMechanics.h @@ -17,6 +17,9 @@ class DLL_LINKAGE AcidBreathDamageMechanics : public DefaultSpellMechanics { public: AcidBreathDamageMechanics(CSpell * s): DefaultSpellMechanics(s){}; + + ESpellCastProblem::ESpellCastProblem isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const override; + protected: void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; }; diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index f7055cd3e..163cedf91 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -34,35 +34,15 @@ BattleSpellCastParameters::Destination::Destination(const BattleHex & destinatio } -BattleSpellCastParameters::BattleSpellCastParameters(const BattleInfo * cb, const ISpellCaster * caster, const CSpell * spell) - : cb(cb), caster(caster), casterColor(caster->getOwner()), casterSide(cb->whatSide(casterColor)), +BattleSpellCastParameters::BattleSpellCastParameters(const BattleInfo * cb, const ISpellCaster * caster, const CSpell * spell_) + : spell(spell_), cb(cb), caster(caster), casterColor(caster->getOwner()), casterSide(cb->whatSide(casterColor)), casterHero(nullptr), - mode(ECastingMode::HERO_CASTING), casterStack(nullptr), selectedStack(nullptr), - spellLvl(-1), effectLevel(-1), effectPower(0), enchantPower(0), effectValue(0) + mode(ECastingMode::HERO_CASTING), casterStack(nullptr), + spellLvl(0), effectLevel(0), effectPower(0), enchantPower(0), effectValue(0) { casterStack = dynamic_cast(caster); casterHero = dynamic_cast(caster); - prepare(spell); -} -void BattleSpellCastParameters::aimToHex(const BattleHex& destination) -{ - destinations.push_back(Destination(destination)); -} - -void BattleSpellCastParameters::aimToStack(const CStack * destination) -{ - destinations.push_back(Destination(destination)); -} - - -BattleHex BattleSpellCastParameters::getFirstDestinationHex() const -{ - return destinations.at(0).hexValue; -} - -void BattleSpellCastParameters::prepare(const CSpell * spell) -{ spellLvl = caster->getSpellSchoolLevel(spell); effectLevel = caster->getEffectLevel(spell); effectPower = caster->getEffectPower(spell); @@ -76,6 +56,43 @@ void BattleSpellCastParameters::prepare(const CSpell * spell) vstd::amax(effectValue, 0); } +BattleSpellCastParameters::BattleSpellCastParameters(const BattleSpellCastParameters & orig, const ISpellCaster * caster) + :spell(orig.spell), cb(orig.cb), caster(caster), casterColor(caster->getOwner()), casterSide(cb->whatSide(casterColor)), + casterHero(nullptr), mode(ECastingMode::MAGIC_MIRROR), casterStack(nullptr), + spellLvl(orig.spellLvl), effectLevel(orig.effectLevel), effectPower(orig.effectPower), enchantPower(orig.enchantPower), effectValue(orig.effectValue) +{ + casterStack = dynamic_cast(caster); + casterHero = dynamic_cast(caster); +} + +void BattleSpellCastParameters::aimToHex(const BattleHex& destination) +{ + destinations.push_back(Destination(destination)); +} + +void BattleSpellCastParameters::aimToStack(const CStack * destination) +{ + if(nullptr == destination) + logGlobal->error("BattleSpellCastParameters::aimToStack invalid stack."); + else + destinations.push_back(Destination(destination)); +} + +void BattleSpellCastParameters::cast(const SpellCastEnvironment * env) +{ + spell->battleCast(env, *this); +} + +BattleHex BattleSpellCastParameters::getFirstDestinationHex() const +{ + return destinations.at(0).hexValue; +} + +int BattleSpellCastParameters::getEffectValue() const +{ + return (effectValue == 0) ? spell->calculateRawEffectValue(effectLevel, effectPower) : effectValue; +} + ///ISpellMechanics ISpellMechanics::ISpellMechanics(CSpell * s): owner(s) @@ -83,72 +100,89 @@ ISpellMechanics::ISpellMechanics(CSpell * s): } -ISpellMechanics * ISpellMechanics::createMechanics(CSpell * s) +std::unique_ptr ISpellMechanics::createMechanics(CSpell * s) { switch (s->id) { case SpellID::ANTI_MAGIC: - return new AntimagicMechanics(s); + return make_unique(s); case SpellID::ACID_BREATH_DAMAGE: - return new AcidBreathDamageMechanics(s); + return make_unique(s); case SpellID::CHAIN_LIGHTNING: - return new ChainLightningMechanics(s); + return make_unique(s); case SpellID::CLONE: - return new CloneMechanics(s); + return make_unique(s); case SpellID::CURE: - return new CureMechanics(s); + return make_unique(s); case SpellID::DEATH_STARE: - return new DeathStareMechanics(s); + return make_unique(s); case SpellID::DISPEL: - return new DispellMechanics(s); + return make_unique(s); case SpellID::DISPEL_HELPFUL_SPELLS: - return new DispellHelpfulMechanics(s); + return make_unique(s); case SpellID::EARTHQUAKE: - return new EarthquakeMechanics(s); + return make_unique(s); case SpellID::FIRE_WALL: + return make_unique(s); case SpellID::FORCE_FIELD: - return new WallMechanics(s); + return make_unique(s); case SpellID::HYPNOTIZE: - return new HypnotizeMechanics(s); + return make_unique(s); case SpellID::LAND_MINE: + return make_unique(s); case SpellID::QUICKSAND: - return new ObstacleMechanics(s); + return make_unique(s); case SpellID::REMOVE_OBSTACLE: - return new RemoveObstacleMechanics(s); + return make_unique(s); case SpellID::SACRIFICE: - return new SacrificeMechanics(s); + return make_unique(s); case SpellID::SUMMON_FIRE_ELEMENTAL: - return new SummonMechanics(s, CreatureID::FIRE_ELEMENTAL); + return make_unique(s, CreatureID::FIRE_ELEMENTAL); case SpellID::SUMMON_EARTH_ELEMENTAL: - return new SummonMechanics(s, CreatureID::EARTH_ELEMENTAL); + return make_unique(s, CreatureID::EARTH_ELEMENTAL); case SpellID::SUMMON_WATER_ELEMENTAL: - return new SummonMechanics(s, CreatureID::WATER_ELEMENTAL); + return make_unique(s, CreatureID::WATER_ELEMENTAL); case SpellID::SUMMON_AIR_ELEMENTAL: - return new SummonMechanics(s, CreatureID::AIR_ELEMENTAL); + return make_unique(s, CreatureID::AIR_ELEMENTAL); case SpellID::TELEPORT: - return new TeleportMechanics(s); + return make_unique(s); + default: + if(s->isRisingSpell()) + return make_unique(s); + else + return make_unique(s); + } +} + +//IAdventureSpellMechanics +IAdventureSpellMechanics::IAdventureSpellMechanics(CSpell * s): + owner(s) +{ + +} + +std::unique_ptr IAdventureSpellMechanics::createMechanics(CSpell * s) +{ + switch (s->id) + { case SpellID::SUMMON_BOAT: - return new SummonBoatMechanics(s); + return make_unique(s); case SpellID::SCUTTLE_BOAT: - return new ScuttleBoatMechanics(s); + return make_unique(s); case SpellID::DIMENSION_DOOR: - return new DimensionDoorMechanics(s); + return make_unique(s); case SpellID::FLY: case SpellID::WATER_WALK: case SpellID::VISIONS: case SpellID::DISGUISE: - return new DefaultSpellMechanics(s); //implemented using bonus system + return make_unique(s); //implemented using bonus system case SpellID::TOWN_PORTAL: - return new TownPortalMechanics(s); + return make_unique(s); case SpellID::VIEW_EARTH: - return new ViewEarthMechanics(s); + return make_unique(s); case SpellID::VIEW_AIR: - return new ViewAirMechanics(s); + return make_unique(s); default: - if(s->isRisingSpell()) - return new SpecialRisingSpellMechanics(s); - else - return new DefaultSpellMechanics(s); + return std::unique_ptr(); } } - diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index c48d0aebd..11aafe856 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -45,11 +45,22 @@ public: const BattleHex hexValue; }; - BattleSpellCastParameters(const BattleInfo * cb, const ISpellCaster * caster, const CSpell * spell); + //normal constructor + BattleSpellCastParameters(const BattleInfo * cb, const ISpellCaster * caster, const CSpell * spell_); + + //magic mirror constructor + BattleSpellCastParameters(const BattleSpellCastParameters & orig, const ISpellCaster * caster); + void aimToHex(const BattleHex & destination); void aimToStack(const CStack * destination); + + void cast(const SpellCastEnvironment * env); + BattleHex getFirstDestinationHex() const; + int getEffectValue() const; + + const CSpell * spell; const BattleInfo * cb; const ISpellCaster * caster; const PlayerColor casterColor; @@ -60,7 +71,6 @@ public: const CGHeroInstance * casterHero; //deprecated ECastingMode::ECastingMode mode; const CStack * casterStack; //deprecated - const CStack * selectedStack;//deprecated ///spell school level int spellLvl; @@ -70,10 +80,50 @@ public: int effectPower; ///actual spell-power affecting effect duration int enchantPower; + +private: ///for Archangel-like casting int effectValue; -private: - void prepare(const CSpell * spell); +}; + +struct DLL_LINKAGE SpellTargetingContext +{ + CSpell::TargetInfo ti; + ECastingMode::ECastingMode mode; + BattleHex destination; + const ISpellCaster * caster; + int schoolLvl; + + SpellTargetingContext(const CSpell * s, ECastingMode::ECastingMode mode_, const ISpellCaster * caster_, int schoolLvl_, BattleHex destination_) + : ti(s,schoolLvl_, mode_), mode(mode_), destination(destination_), caster(caster_), schoolLvl(schoolLvl_) + {}; + +}; + +class DLL_LINKAGE ISpellMechanics +{ +public: + ISpellMechanics(CSpell * s); + virtual ~ISpellMechanics(){}; + + virtual std::vector rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool * outDroppedHexes = nullptr) const = 0; + virtual std::vector getAffectedStacks(const CBattleInfoCallback * cb, SpellTargetingContext & ctx) const = 0; + + virtual ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const ECastingMode::ECastingMode mode, const ISpellCaster * caster) const = 0; + + virtual ESpellCastProblem::ESpellCastProblem canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const = 0; + + virtual ESpellCastProblem::ESpellCastProblem isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const = 0; + + virtual void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const = 0; + virtual void battleCast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters) const = 0; + + //if true use generic algorithm for target existence check, see CSpell::canBeCast + virtual bool requiresCreatureTarget() const = 0; + + static std::unique_ptr createMechanics(CSpell * s); +protected: + CSpell * owner; }; struct DLL_LINKAGE AdventureSpellCastParameters @@ -82,42 +132,15 @@ struct DLL_LINKAGE AdventureSpellCastParameters int3 pos; }; -class DLL_LINKAGE ISpellMechanics +class DLL_LINKAGE IAdventureSpellMechanics { public: - struct DLL_LINKAGE SpellTargetingContext - { - const CBattleInfoCallback * cb; - CSpell::TargetInfo ti; - ECastingMode::ECastingMode mode; - BattleHex destination; - PlayerColor casterColor; - int schoolLvl; + IAdventureSpellMechanics(CSpell * s); + virtual ~IAdventureSpellMechanics() = default; - 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 canBeCast(const CBattleInfoCallback * cb, PlayerColor player) const = 0; - - virtual ESpellCastProblem::ESpellCastProblem isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const = 0; - - virtual void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const = 0; virtual bool adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const = 0; - virtual void battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const = 0; - virtual void battleLogSingleTarget(std::vector & logLines, const BattleSpellCast * packet, - const std::string & casterName, const CStack * attackedStack, bool & displayDamage) const = 0; - - static ISpellMechanics * createMechanics(CSpell * s); + static std::unique_ptr createMechanics(CSpell * s); protected: CSpell * owner; }; diff --git a/lib/spells/Magic.h b/lib/spells/Magic.h index 5055fef35..32b43ad16 100644 --- a/lib/spells/Magic.h +++ b/lib/spells/Magic.h @@ -18,6 +18,7 @@ class CSpell; class CStack; class PlayerColor; +struct MetaString; class DLL_LINKAGE ISpellCaster { @@ -45,4 +46,10 @@ public: virtual int getEffectValue(const CSpell * spell) const = 0; virtual const PlayerColor getOwner() const = 0; + + ///only name substitution + virtual void getCasterName(MetaString & text) const = 0; + + ///full default text + virtual void getCastDescription(const CSpell * spell, const std::vector & attacked, MetaString & text) const = 0; }; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 20aa2a29b..18f8089f6 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -846,7 +846,7 @@ void CGameHandler::prepareAttack(BattleAttack &bat, const CStack *att, const CSt //TODO: should spell override creature`s projectile? - std::set attackedCreatures = SpellID(bonus->subtype).toSpell()->getAffectedStacks(gs->curB, ECastingMode::SPELL_LIKE_ATTACK, att->owner, bonus->val, targetHex, att); + auto attackedCreatures = SpellID(bonus->subtype).toSpell()->getAffectedStacks(gs->curB, ECastingMode::SPELL_LIKE_ATTACK, att, bonus->val, targetHex); //TODO: get exact attacked hex for defender @@ -2520,7 +2520,7 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) void CGameHandler::sendToAllClients( CPackForClient * info ) { - logGlobal->trace("Sending to all clients a package of type %s", typeid(*info).name()); + logNetwork->trace("Sending to all clients a package of type %s", typeid(*info).name()); for(auto & elem : conns) { boost::unique_lock lock(*(elem)->wmx); @@ -4179,7 +4179,7 @@ bool CGameHandler::makeBattleAction( BattleAction &ba ) bsa.creID = summonedType; ui64 risedHp = summoner->count * summoner->valOfBonuses(Bonus::DAEMON_SUMMONING, bsa.creID.toEnum()); - ui64 targetHealth = destStack->getCreature()->MaxHealth() * destStack->baseAmount;//todo: ignore AGE effect + ui64 targetHealth = destStack->getCreature()->MaxHealth() * destStack->baseAmount; ui64 canRiseHp = std::min(targetHealth, risedHp); ui32 canRiseAmount = canRiseHp / VLC->creh->creatures.at(bsa.creID)->MaxHealth(); @@ -4241,8 +4241,7 @@ bool CGameHandler::makeBattleAction( BattleAction &ba ) parameters.effectLevel = parameters.spellLvl; parameters.mode = ECastingMode::CREATURE_ACTIVE_CASTING; parameters.aimToHex(destination);//todo: allow multiple destinations - parameters.selectedStack = nullptr; - spell->battleCast(spellEnv, parameters); + parameters.cast(spellEnv); } sendAndApply(&end_action); break; @@ -4440,7 +4439,8 @@ bool CGameHandler::makeCustomAction( BattleAction &ba ) BattleSpellCastParameters parameters(gs->curB, h, s); parameters.aimToHex(ba.destinationTile);//todo: allow multiple destinations parameters.mode = ECastingMode::HERO_CASTING; - parameters.selectedStack = gs->curB->battleGetStackByID(ba.selectedStack, false); + if(ba.selectedStack >= 0) + parameters.aimToStack(gs->curB->battleGetStackByID(ba.selectedStack, false)); ESpellCastProblem::ESpellCastProblem escp = gs->curB->battleCanCastThisSpell(h, s, ECastingMode::HERO_CASTING);//todo: should we check aimed cast(battleCanCastThisSpellHere)? if(escp != ESpellCastProblem::OK) @@ -4452,7 +4452,7 @@ bool CGameHandler::makeCustomAction( BattleAction &ba ) StartAction start_action(ba); sendAndApply(&start_action); //start spell casting - s->battleCast(spellEnv, parameters); + parameters.cast(spellEnv); sendAndApply(&end_action); if( !gs->curB->battleGetStackByID(gs->curB->activeStack)) @@ -4592,9 +4592,8 @@ void CGameHandler::stackTurnTrigger(const CStack * st) parameters.effectLevel = bonus->val;//todo: recheck parameters.aimToHex(BattleHex::INVALID); parameters.mode = ECastingMode::ENCHANTER_CASTING; - parameters.selectedStack = nullptr; - spell->battleCast(spellEnv, parameters); + parameters.cast(spellEnv); //todo: move to mechanics BattleSetStackProperty ssp; @@ -5303,9 +5302,8 @@ void CGameHandler::attackCasting(const BattleAttack & bat, Bonus::BonusType atta parameters.effectLevel = spellLevel; parameters.aimToStack(oneOfAttacked); parameters.mode = ECastingMode::AFTER_ATTACK_CASTING; - parameters.selectedStack = nullptr; - spell->battleCast(spellEnv, parameters); + parameters.cast(spellEnv); } } } @@ -5333,9 +5331,8 @@ void CGameHandler::handleAfterAttackCasting( const BattleAttack & bat ) parameters.aimToStack(gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)); parameters.effectPower = power; parameters.mode = ECastingMode::AFTER_ATTACK_CASTING; - parameters.selectedStack = nullptr; - spell->battleCast(this->spellEnv, parameters); + parameters.cast(spellEnv); }; attackCasting(bat, Bonus::SPELL_AFTER_ATTACK, attacker); @@ -5356,9 +5353,8 @@ void CGameHandler::handleAfterAttackCasting( const BattleAttack & bat ) vstd::amin(chanceToKill, 1); //cap at 100% std::binomial_distribution<> distribution(attacker->count, chanceToKill); - std::mt19937 rng(std::time(nullptr)); - int staredCreatures = distribution(rng); + int staredCreatures = distribution(getRandomGenerator().getStdGenerator()); double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 int maxToKill = (attacker->count + cap - 1) / cap; //not much more than chance * count @@ -5628,21 +5624,24 @@ void CGameHandler::runBattle() for(int i = 0; i < 2; ++i) { auto h = gs->curB->battleGetFightingHero(i); - if(h && h->hasBonusOfType(Bonus::OPENING_BATTLE_SPELL)) + if(h) { TBonusListPtr bl = h->getBonuses(Selector::type(Bonus::OPENING_BATTLE_SPELL)); for (Bonus *b : *bl) { const CSpell * spell = SpellID(b->subtype).toSpell(); + + if(ESpellCastProblem::OK != gs->curB->battleCanCastThisSpell(h, spell, ECastingMode::PASSIVE_CASTING)) + continue; + BattleSpellCastParameters parameters(gs->curB, h, spell); parameters.spellLvl = 3; parameters.effectLevel = 3; parameters.aimToHex(BattleHex::INVALID); parameters.mode = ECastingMode::PASSIVE_CASTING; - parameters.selectedStack = nullptr; parameters.enchantPower = b->val; - spell->battleCast(spellEnv, parameters); + parameters.cast(spellEnv); } } } diff --git a/server/CQuery.cpp b/server/CQuery.cpp index 886b478f4..2def7689e 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -126,7 +126,7 @@ void CObjectVisitQuery::onExposure(CGameHandler *gh, QueryPtr topQuery) void Queries::popQuery(PlayerColor player, QueryPtr query) { - LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); + //LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); if(topQuery(player) != query) { logGlobal->trace("Cannot remove, not a top!"); @@ -147,7 +147,7 @@ void Queries::popQuery(PlayerColor player, QueryPtr query) void Queries::popQuery(const CQuery &query) { - LOG_TRACE_PARAMS(logGlobal, "query='%s'", query); + //LOG_TRACE_PARAMS(logGlobal, "query='%s'", query); assert(query.players.size()); for(auto player : query.players) @@ -174,7 +174,7 @@ void Queries::addQuery(QueryPtr query) void Queries::addQuery(PlayerColor player, QueryPtr query) { - LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query); + //LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query); query->onAdding(gh, player); queries[player].push_back(query); } @@ -186,7 +186,7 @@ QueryPtr Queries::topQuery(PlayerColor player) void Queries::popIfTop(QueryPtr query) { - LOG_TRACE_PARAMS(logGlobal, "query='%d'", query); + //LOG_TRACE_PARAMS(logGlobal, "query='%d'", query); if(!query) logGlobal->error("The query is nullptr! Ignoring.");