From 0f5202689e13f9994b36f691db0087ecdeb4a3b1 Mon Sep 17 00:00:00 2001 From: AlexVinS Date: Wed, 2 Nov 2016 20:11:01 +0300 Subject: [PATCH] Cumulative spell effects * Added experimental support for cumulative effects for ENCHANTED bonus * Updated and fixed SPECIAL_PECULIAR_ENCHANT processing * Initial implementation of cumulative spell effects. * Scheme for new spell feature - cumulative bonus. --- AI/BattleAI/BattleAI.cpp | 8 +- ChangeLog | 3 + client/battle/CBattleInterface.cpp | 30 ++-- config/schemas/spell.json | 45 ++--- config/spells/ability.json | 3 +- config/spells/timed.json | 8 +- lib/CStack.cpp | 15 -- lib/CStack.h | 1 - lib/NetPacks.h | 8 + lib/NetPacksLib.cpp | 29 +++- lib/serializer/CSerializer.h | 2 +- lib/spells/AdventureSpellMechanics.cpp | 3 +- lib/spells/CDefaultSpellMechanics.cpp | 223 +++++++++++++------------ lib/spells/CDefaultSpellMechanics.h | 3 + lib/spells/CSpellHandler.cpp | 50 ++++-- lib/spells/CSpellHandler.h | 38 ++++- server/CGameHandler.cpp | 33 ++-- server/CGameHandler.h | 2 +- 18 files changed, 292 insertions(+), 212 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 275cfc5c2..18a0a32a1 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -259,11 +259,9 @@ void CBattleAI::attemptCastingSpell() { StackWithBonuses swb; swb.stack = sta; - Bonus pseudoBonus; - pseudoBonus.sid = ps.spell->id; - pseudoBonus.val = skillLevel; - pseudoBonus.turnsRemain = 1; //TODO - CStack::stackEffectToFeature(swb.bonusesToAdd, pseudoBonus); + //todo: handle effect actualization in HypotheticChangesToBattleState + ps.spell->getEffects(swb.bonusesToAdd, skillLevel, false, hero->getEnchantPower(ps.spell)); + ps.spell->getEffects(swb.bonusesToAdd, skillLevel, true, hero->getEnchantPower(ps.spell)); HypotheticChangesToBattleState state; state.bonusesOfStacks[swb.stack] = &swb; PotentialTargets pt(swb.stack, state); diff --git a/ChangeLog b/ChangeLog index afb527b7b..ebafdf5e9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,9 @@ GENERAL: * Spectator mode was implemented through command-line options +SPELLS: +* Implemented cumulative effects for spells + 0.98 -> 0.99 GENERAL: diff --git a/client/battle/CBattleInterface.cpp b/client/battle/CBattleInterface.cpp index e1c61e087..170b3bcfc 100644 --- a/client/battle/CBattleInterface.cpp +++ b/client/battle/CBattleInterface.cpp @@ -1359,52 +1359,44 @@ void CBattleInterface::spellCast(const BattleSpellCast *sc) void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) { - if (sse.effect.back().sid == -1 && sse.stacks.size() == 1 && sse.effect.size() == 2) + if(sse.stacks.size() == 1 && sse.effect.size() == 2 && sse.effect.back().sid == -1) { const Bonus & bns = sse.effect.front(); - if (bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL) + if(bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL) { //defensive stance const CStack *stack = LOCPLINT->cb->battleGetStackByID(*sse.stacks.begin()); int txtid = 120; - if (stack->count != 1) + if(stack->count != 1) txtid++; //move to plural text BonusList defenseBonuses = *(stack->getBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE))); - defenseBonuses.remove_if (Bonus::UntilGetsTurn); //remove bonuses gained from defensive stance + defenseBonuses.remove_if(Bonus::UntilGetsTurn); //remove bonuses gained from defensive stance int val = stack->Defense() - defenseBonuses.totalValue(); - auto txt = boost::format (CGI->generaltexth->allTexts[txtid]) % ((stack->count != 1) ? stack->getCreature()->namePl : stack->getCreature()->nameSing) % val; + auto txt = boost::format(CGI->generaltexth->allTexts[txtid]) % ((stack->count != 1) ? stack->getCreature()->namePl : stack->getCreature()->nameSing) % val; console->addText(boost::to_string(txt)); } } - if (activeStack != nullptr) //it can be -1 when a creature casts effect - { + if(activeStack != nullptr) redrawBackgroundWithHexes(activeStack); - } } -CBattleInterface::PossibleActions CBattleInterface::getCasterAction(const CSpell *spell, const ISpellCaster *caster, ECastingMode::ECastingMode mode) const +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) + if(ti.massive || ti.type == CSpell::NO_TARGET) spellSelMode = NO_LOCATION; - else if (ti.type == CSpell::LOCATION && ti.clearAffected) - { + else if(ti.type == CSpell::LOCATION && ti.clearAffected) spellSelMode = FREE_LOCATION; - } - else if (ti.type == CSpell::CREATURE) - { + else if(ti.type == CSpell::CREATURE) spellSelMode = AIMED_SPELL_CREATURE; - } - else if (ti.type == CSpell::OBSTACLE) - { + else if(ti.type == CSpell::OBSTACLE) spellSelMode = OBSTACLE; - } return spellSelMode; } diff --git a/config/schemas/spell.json b/config/schemas/spell.json index e63ef3a84..e7f1061cb 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -19,25 +19,25 @@ { //assumed verticalPosition: top "type": "string", - "format": "defFile" + "format": "defFile" }, { "type": "object", "properties":{ "verticalPosition": {"type":"string", "enum":["top","bottom"]}, "defName": {"type":"string", "format": "defFile"} - }, - "additionalProperties" : false + }, + "additionalProperties" : false } - ] - } + ] + } }, "animation":{ "type": "object", "additionalProperties" : false, "properties":{ - "affect":{"$ref" : "#/definitions/animationQueue"}, - "hit":{"$ref" : "#/definitions/animationQueue"}, + "affect":{"$ref" : "#/definitions/animationQueue"}, + "hit":{"$ref" : "#/definitions/animationQueue"}, "cast":{"$ref" : "#/definitions/animationQueue"}, "projectile":{ "type":"array", @@ -46,11 +46,11 @@ "properties":{ "minimumAngle": {"type":"number", "minimum" : 0}, "defName": {"type":"string", "format": "defFile"} - }, - "additionalProperties" : false + }, + "additionalProperties" : false } - } - } + } + } }, "flags" :{ "type" : "object", @@ -85,7 +85,14 @@ }, "effects":{ "type": "object", - "description": "Timed effects", + "description": "Timed effects (updated by prolongation)", + "additionalProperties" : { + "$ref" : "vcmi:bonus" + } + }, + "cumulativeEffects":{ + "type": "object", + "description": "Timed effects (updated by unique bonus)", "additionalProperties" : { "$ref" : "vcmi:bonus" } @@ -107,16 +114,16 @@ { "type": "boolean", "description": "LOCATION target only. All affected hexes/tile must be clear" - } + } } } } }, - + "texts":{ "type": "object", - - + + "additionalProperties" : false } }, @@ -239,9 +246,9 @@ "$ref" : "#/definitions/flags", "description": "flags structure of bonus names, presence of all bonuses required to be affected by, can't be negated." }, - + "animation":{"$ref": "#/definitions/animation"}, - + "graphics":{ "type": "object", "additionalProperties" : false, @@ -291,7 +298,7 @@ "additionalProperties" : false, "required" : ["none", "basic", "advanced", "expert"], - "properties":{ + "properties":{ "base":{ "type": "object", "description": "will be merged with all levels", diff --git a/config/spells/ability.json b/config/spells/ability.json index bcec994d8..347d4bb61 100644 --- a/config/spells/ability.json +++ b/config/spells/ability.json @@ -101,6 +101,7 @@ "levels" : { "base":{ "range" : "0", + //no cumulative effect even with mods here "effects" : { "bindEffect" : { "val" : 0, @@ -335,7 +336,7 @@ "base":{ "range" : "0", "targetModifier":{"smart":true}, - "effects" : { + "cumulativeEffects" : { "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", diff --git a/config/spells/timed.json b/config/spells/timed.json index a54735fca..84bd3b472 100644 --- a/config/spells/timed.json +++ b/config/spells/timed.json @@ -13,6 +13,7 @@ "base":{ "range" : "0", "targetModifier":{"smart":true}, + //no cumulative effect even with mods here "effects" : { "generalDamageReduction" : { "type" : "GENERAL_DAMAGE_REDUCTION", @@ -43,6 +44,7 @@ "base":{ "range" : "0", "targetModifier":{"smart":true}, + //no cumulative effect even with mods here "effects" : { "generalDamageReduction" : { "type" : "GENERAL_DAMAGE_REDUCTION", @@ -558,7 +560,7 @@ "base":{ "range" : "0", "targetModifier":{"smart":true}, - "effects" : { + "cumulativeEffects" : { "primarySkill" : { "type" : "PRIMARY_SKILL", "subtype" : "primSkill.defence", @@ -569,14 +571,14 @@ } }, "advanced":{ - "effects" : { + "cumulativeEffects" : { "primarySkill" : { "val" : -4 } } }, "expert":{ - "effects" : { + "cumulativeEffects" : { "primarySkill" : { "val" : -5 } diff --git a/lib/CStack.cpp b/lib/CStack.cpp index 4eef55bb1..3736dddfc 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -104,21 +104,6 @@ si32 CStack::magicResistance() const return magicResistance; } -void CStack::stackEffectToFeature(std::vector & sf, const Bonus & sse) -{ - const CSpell * sp = SpellID(sse.sid).toSpell(); - - std::vector tmp; - sp->getEffects(tmp, sse.val); - - for(Bonus& b : tmp) - { - if(b.turnsRemain == 0) - b.turnsRemain = sse.turnsRemain; - sf.push_back(b); - } -} - bool CStack::willMove(int turn /*= 0*/) const { return ( turn ? true : !vstd::contains(state, EBattleStackState::DEFENDING) ) diff --git a/lib/CStack.h b/lib/CStack.h index 75cc150b0..86f1bde8f 100644 --- a/lib/CStack.h +++ b/lib/CStack.h @@ -62,7 +62,6 @@ public: ui32 calculateHealedHealthPoints(ui32 toHeal, const bool resurrect) const; ui32 level() const; si32 magicResistance() const override; //include aura of resistance - static void stackEffectToFeature(std::vector & sf, const Bonus & sse); std::vector activeSpells() const; //returns vector of active spell IDs sorted by time of cast const CGHeroInstance *getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise ui32 totalHealth() const; // total health for all creatures in stack; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index e353e5715..700e43d7b 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1519,11 +1519,19 @@ struct SetStackEffect : public CPackForClient void applyCl(CClient *cl); std::vector stacks; //affected stacks (IDs) + + //regular effects std::vector effect; //bonuses to apply std::vector > uniqueBonuses; //bonuses per single stack + + //cumulative effects + std::vector cumulativeEffects; //bonuses to apply + std::vector > cumulativeUniqueBonuses; //bonuses per single stack + template void serialize(Handler &h, const int version) { h & stacks & effect & uniqueBonuses; + h & cumulativeEffects & cumulativeUniqueBonuses; } }; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 0268c5727..b49f91bae 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1554,33 +1554,39 @@ void actualizeEffect(CStack * s, const std::vector & ef) DLL_LINKAGE void SetStackEffect::applyGs(CGameState *gs) { - if(effect.empty()) + if(effect.empty() && cumulativeEffects.empty()) { logGlobal->errorStream() << "Trying to apply SetStackEffect with no effects"; return; } - int spellid = effect.begin()->sid; //effects' source ID + si32 spellid = effect.empty() ? cumulativeEffects.begin()->sid : effect.begin()->sid; //effects' source ID - auto processEffect = [spellid, this](CStack * sta, const Bonus & effect) + auto processEffect = [spellid, this](CStack * sta, const Bonus & effect, bool cumulative) { - if(!sta->hasBonus(Selector::source(Bonus::SPELL_EFFECT, spellid).And(Selector::typeSubtype(effect.type, effect.subtype))) - || spellid == SpellID::DISRUPTING_RAY || spellid == SpellID::ACID_BREATH_DEFENSE) + if(cumulative || !sta->hasBonus(Selector::source(Bonus::SPELL_EFFECT, spellid).And(Selector::typeSubtype(effect.type, effect.subtype)))) { //no such effect or cumulative - add new logBonus->traceStream() << sta->nodeName() << " receives a new bonus: " << effect.Description(); sta->addNewBonus(std::make_shared(effect)); } else + { + logBonus->traceStream() << sta->nodeName() << " updated bonus: " << effect.Description(); actualizeEffect(sta, effect); + } }; for(ui32 id : stacks) { CStack *s = gs->curB->getStack(id); if(s) + { for(const Bonus & fromEffect : effect) - processEffect(s, fromEffect); + processEffect(s, fromEffect, false); + for(const Bonus & fromEffect : cumulativeEffects) + processEffect(s, fromEffect, true); + } else logNetwork->errorStream() << "Cannot find stack " << id; } @@ -1589,7 +1595,16 @@ DLL_LINKAGE void SetStackEffect::applyGs(CGameState *gs) { CStack *s = gs->curB->getStack(para.first); if(s) - processEffect(s, para.second); + processEffect(s, para.second, false); + else + logNetwork->errorStream() << "Cannot find stack " << para.first; + } + + for(auto & para : cumulativeUniqueBonuses) + { + CStack *s = gs->curB->getStack(para.first); + if(s) + processEffect(s, para.second, true); else logNetwork->errorStream() << "Cannot find stack " << para.first; } diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 0b72b2fc6..d19f91c7f 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,7 +14,7 @@ #include "../ConstTransitivePtr.h" #include "../GameConstants.h" -const ui32 SERIALIZATION_VERSION = 771; +const ui32 SERIALIZATION_VERSION = 773; const ui32 MINIMAL_SERIALIZATION_VERSION = 753; const std::string SAVEGAME_MAGIC = "VCMISVG"; diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 4f7984d77..9db4394e5 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -79,11 +79,12 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(const SpellCastE { if(owner->hasEffects()) { + //todo: cumulative effects support const int schoolLevel = parameters.caster->getSpellSchoolLevel(owner); std::vector bonuses; - owner->getEffects(bonuses, schoolLevel); + owner->getEffects(bonuses, schoolLevel, false, parameters.caster->getEnchantPower(owner)); for(Bonus b : bonuses) { diff --git a/lib/spells/CDefaultSpellMechanics.cpp b/lib/spells/CDefaultSpellMechanics.cpp index df7322cc5..4bd4ee546 100644 --- a/lib/spells/CDefaultSpellMechanics.cpp +++ b/lib/spells/CDefaultSpellMechanics.cpp @@ -445,120 +445,133 @@ void DefaultSpellMechanics::battleLogDefault(std::vector & logLines, void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const { - //applying effects if(owner->isOffensiveSpell()) - { - const int rawDamage = parameters.getEffectValue(); - int chainLightningModifier = 0; - for(auto & attackedCre : ctx.attackedCres) - { - BattleStackAttacked bsa; - 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 - bsa.attackerID = parameters.casterStack->ID; - else - bsa.attackerID = -1; - (attackedCre)->prepareAttacked(bsa, env->getRandomGenerator()); - ctx.si.stacks.push_back(bsa); - - if(owner->id == SpellID::CHAIN_LIGHTNING) - ++chainLightningModifier; - } - } + defaultDamageEffect(env, parameters, ctx); if(owner->hasEffects()) + defaultTimedEffect(env, parameters, ctx); +} + +void DefaultSpellMechanics::defaultDamageEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const +{ + const int rawDamage = parameters.getEffectValue(); + int chainLightningModifier = 0; + for(auto & attackedCre : ctx.attackedCres) { - SetStackEffect sse; - //get default spell duration (spell power with bonuses for heroes) - int duration = parameters.enchantPower; - //generate actual stack bonuses - { - int maxDuration = 0; - std::vector tmp; - owner->getEffects(tmp, parameters.effectLevel); - for(Bonus& b : tmp) - { - //use configured duration if present - if(b.turnsRemain == 0) - b.turnsRemain = duration; - vstd::amax(maxDuration, b.turnsRemain); - sse.effect.push_back(b); - } - //if all spell effects have special duration, use it - duration = maxDuration; - } - //fix to original config: shield should display damage reduction - if(owner->id == SpellID::SHIELD || owner->id == SpellID::AIR_SHIELD) - { - sse.effect.back().val = (100 - sse.effect.back().val); - } - //we need to know who cast Bind - if(owner->id == SpellID::BIND && parameters.casterStack) - { - sse.effect.back().additionalInfo = parameters.casterStack->ID; - } - std::shared_ptr bonus = nullptr; - if(parameters.casterHero) - bonus = parameters.casterHero->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPECIAL_PECULIAR_ENCHANT, owner->id)); - //TODO does hero specialty should affects his stack casting spells? + BattleStackAttacked bsa; + bsa.damageAmount = owner->adjustRawDamage(parameters.caster, attackedCre, rawDamage) >> chainLightningModifier; + ctx.addDamageToDisplay(bsa.damageAmount); - si32 power = 0; - for(const CStack * affected : ctx.attackedCres) - { - sse.stacks.push_back(affected->ID); + bsa.stackAttacked = (attackedCre)->ID; + if(parameters.mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast + bsa.attackerID = parameters.casterStack->ID; + else + bsa.attackerID = -1; + (attackedCre)->prepareAttacked(bsa, env->getRandomGenerator()); + ctx.si.stacks.push_back(bsa); - //Apply hero specials - peculiar enchants - const ui8 tier = std::max((ui8)1, affected->getCreature()->level); //don't divide by 0 for certain creatures (commanders, war machines) - if(bonus) - { - switch(bonus->additionalInfo) - { - case 0: //normal - { - switch(tier) - { - case 1: case 2: - power = 3; - break; - case 3: case 4: - power = 2; - break; - case 5: case 6: - power = 1; - break; - } - Bonus specialBonus(sse.effect.back()); - specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely - sse.uniqueBonuses.push_back (std::pair (affected->ID, specialBonus)); //additional premy to given effect - } - break; - case 1: //only Coronius as yet - { - power = std::max(5 - tier, 0); - Bonus specialBonus(Bonus::N_TURNS, Bonus::PRIMARY_SKILL, Bonus::SPELL_EFFECT, power, owner->id, PrimarySkill::ATTACK); - specialBonus.turnsRemain = duration; - sse.uniqueBonuses.push_back(std::pair (affected->ID, specialBonus)); //additional attack to Slayer effect - } - break; - } - } - if (parameters.casterHero && parameters.casterHero->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, owner->id)) //TODO: better handling of bonus percentages - { - int damagePercent = parameters.casterHero->level * parameters.casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, owner->id.toEnum()) / tier; - Bonus specialBonus(Bonus::N_TURNS, Bonus::CREATURE_DAMAGE, Bonus::SPELL_EFFECT, damagePercent, owner->id, 0, Bonus::PERCENT_TO_ALL); - specialBonus.turnsRemain = duration; - sse.uniqueBonuses.push_back (std::pair (affected->ID, specialBonus)); - } - } - - if(!sse.stacks.empty()) - env->sendAndApply(&sse); + if(owner->id == SpellID::CHAIN_LIGHTNING) + ++chainLightningModifier; } } +void DefaultSpellMechanics::defaultTimedEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const +{ + SetStackEffect sse; + //get default spell duration (spell power with bonuses for heroes) + int duration = parameters.enchantPower; + //generate actual stack bonuses + { + si32 maxDuration = 0; + + owner->getEffects(sse.effect, parameters.effectLevel, false, duration, &maxDuration); + owner->getEffects(sse.cumulativeEffects, parameters.effectLevel, true, duration, &maxDuration); + + //if all spell effects have special duration, use it later for special bonuses + duration = maxDuration; + } + //fix to original config: shield should display damage reduction + if(owner->id == SpellID::SHIELD || owner->id == SpellID::AIR_SHIELD) + { + sse.effect.at(sse.effect.size() - 1).val = (100 - sse.effect.back().val); + } + //we need to know who cast Bind + else if(owner->id == SpellID::BIND && parameters.casterStack) + { + sse.effect.at(sse.effect.size() - 1).additionalInfo = parameters.casterStack->ID; + } + std::shared_ptr bonus = nullptr; + if(parameters.casterHero) + bonus = parameters.casterHero->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPECIAL_PECULIAR_ENCHANT, owner->id)); + //TODO does hero specialty should affects his stack casting spells? + + for(const CStack * affected : ctx.attackedCres) + { + si32 power = 0; + sse.stacks.push_back(affected->ID); + + //Apply hero specials - peculiar enchants + const ui8 tier = std::max((ui8)1, affected->getCreature()->level); //don't divide by 0 for certain creatures (commanders, war machines) + if(bonus) + { + switch(bonus->additionalInfo) + { + case 0: //normal + { + switch(tier) + { + case 1: + case 2: + power = 3; + break; + case 3: + case 4: + power = 2; + break; + case 5: + case 6: + power = 1; + break; + } + for(const Bonus & b : sse.effect) + { + Bonus specialBonus(b); + specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely + specialBonus.turnsRemain = duration; + sse.uniqueBonuses.push_back(std::pair(affected->ID, specialBonus)); //additional premy to given effect + } + for(const Bonus & b : sse.cumulativeEffects) + { + Bonus specialBonus(b); + specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely + specialBonus.turnsRemain = duration; + sse.cumulativeUniqueBonuses.push_back(std::pair(affected->ID, specialBonus)); //additional premy to given effect + } + } + break; + case 1: //only Coronius as yet + { + power = std::max(5 - tier, 0); + Bonus specialBonus(Bonus::N_TURNS, Bonus::PRIMARY_SKILL, Bonus::SPELL_EFFECT, power, owner->id, PrimarySkill::ATTACK); + specialBonus.turnsRemain = duration; + sse.uniqueBonuses.push_back(std::pair(affected->ID, specialBonus)); //additional attack to Slayer effect + } + break; + } + } + if(parameters.casterHero && parameters.casterHero->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, owner->id)) //TODO: better handling of bonus percentages + { + int damagePercent = parameters.casterHero->level * parameters.casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, owner->id.toEnum()) / tier; + Bonus specialBonus(Bonus::N_TURNS, Bonus::CREATURE_DAMAGE, Bonus::SPELL_EFFECT, damagePercent, owner->id, 0, Bonus::PERCENT_TO_ALL); + specialBonus.turnsRemain = duration; + sse.uniqueBonuses.push_back(std::pair(affected->ID, specialBonus)); + } + } + + if(!sse.stacks.empty()) + env->sendAndApply(&sse); +} + std::vector DefaultSpellMechanics::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const { using namespace SRSLPraserHelpers; diff --git a/lib/spells/CDefaultSpellMechanics.h b/lib/spells/CDefaultSpellMechanics.h index f3b303374..b2eea19e6 100644 --- a/lib/spells/CDefaultSpellMechanics.h +++ b/lib/spells/CDefaultSpellMechanics.h @@ -75,6 +75,9 @@ protected: protected: void doDispell(BattleInfo * battle, const BattleSpellCast * packet, const CSelector & selector) const; bool canDispell(const IBonusBearer * obj, const CSelector & selector, const std::string &cachingStr = "") const; + + void defaultDamageEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const; + void defaultTimedEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const; private: void cast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, std::vector & reflected) const; diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 7ea37f1dc..38e20c3fd 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -350,7 +350,7 @@ bool CSpell::isSpecialSpell() const bool CSpell::hasEffects() const { - return !levels[0].effects.empty(); + return !levels[0].effects.empty() || !levels[0].cumulativeEffects.empty(); } const std::string & CSpell::getIconImmune() const @@ -382,7 +382,7 @@ si32 CSpell::getProbability(const TFaction factionId) const return probabilities.at(factionId); } -void CSpell::getEffects(std::vector & lst, const int level) const +void CSpell::getEffects(std::vector & lst, const int level, const bool cumulative, const si32 duration, boost::optional maxDuration/* = boost::none*/) const { if(level < 0 || level >= GameConstants::SPELL_SCHOOL_LEVELS) { @@ -390,19 +390,29 @@ void CSpell::getEffects(std::vector & lst, const int level) const return; } - const std::vector & effects = levels[level].effects; + const auto & levelObject = levels.at(level); - if(effects.empty()) + if(levelObject.effects.empty() && levelObject.cumulativeEffects.empty()) { logGlobal->errorStream() << __FUNCTION__ << " This spell (" + name + ") has no effects for level " << level; return; } + const auto & effects = cumulative ? levelObject.cumulativeEffects : levelObject.effects; + lst.reserve(lst.size() + effects.size()); - for(const Bonus & b : effects) + for(const auto b : effects) { - lst.push_back(Bonus(b)); + Bonus nb(*b); + + //use configured duration if present + if(nb.turnsRemain == 0) + nb.turnsRemain = duration; + if(maxDuration) + vstd::amax(*(maxDuration.get()), nb.turnsRemain); + + lst.push_back(nb); } } @@ -1029,9 +1039,25 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode & json, const std::string & if(usePowerAsValue) b->val = levelPower; - levelObject.effectsTmp.push_back(b); + levelObject.effects.push_back(b); } + for(const auto & elem : levelNode["cumulativeEffects"].Struct()) + { + const JsonNode & bonusNode = elem.second; + auto b = JsonUtils::parseBonus(bonusNode); + const bool usePowerAsValue = bonusNode["val"].isNull(); + + //TODO: make this work. see CSpellHandler::afterLoadFinalization() + //b->sid = spell->id; //for all + + b->source = Bonus::SPELL_EFFECT;//for all + + if(usePowerAsValue) + b->val = levelPower; + + levelObject.cumulativeEffects.push_back(b); + } } return spell; @@ -1044,14 +1070,10 @@ void CSpellHandler::afterLoadFinalization() { for(auto & level: spell->levels) { - for(auto bonus : level.effectsTmp) - { - level.effects.push_back(*bonus); - } - level.effectsTmp.clear(); - for(auto & bonus: level.effects) - bonus.sid = spell->id; + bonus->sid = spell->id; + for(auto & bonus: level.cumulativeEffects) + bonus->sid = spell->id; } spell->setup(); } diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index 41afe440c..a60d84d68 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -130,16 +130,35 @@ public: bool clearAffected; std::string range; - std::vector effects; - - std::vector> effectsTmp; //TODO: this should replace effects + std::vector> effects; + std::vector> cumulativeEffects; LevelInfo(); ~LevelInfo(); template void serialize(Handler &h, const int version) { - h & description & cost & power & AIValue & smartTarget & range & effects; + h & description & cost & power & AIValue & smartTarget & range; + + if(version >= 773) + { + h & effects & cumulativeEffects; + } + else + { + //all old effects treated as not cumulative, special cases handled by CSpell::serialize + std::vector old; + h & old; + + if(!h.saving) + { + effects.clear(); + cumulativeEffects.clear(); + for(const Bonus & oldBonus : old) + effects.push_back(std::make_shared(oldBonus)); + } + } + h & clearTarget & clearAffected; } }; @@ -180,7 +199,7 @@ public: si32 level; - std::map school; //todo: use this instead of separate boolean fields + std::map school; si32 power; //spell's power @@ -215,8 +234,7 @@ public: bool isSpecialSpell() const; bool hasEffects() const; - void getEffects(std::vector &lst, const int level) const; - + void getEffects(std::vector & lst, const int level, const bool cumulative, const si32 duration, boost::optional maxDuration = boost::none) const; ///calculate spell damage on stack taking caster`s secondary skills and affectedCreature`s bonuses into account ui32 calculateDamage(const ISpellCaster * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const; @@ -264,6 +282,12 @@ public: if(!h.saving) setup(); + //backward compatibility + //can not be added to level structure as level structure does not know spell id + if(!h.saving && version < 773) + if(id == SpellID::DISRUPTING_RAY || id == SpellID::ACID_BREATH_DEFENSE) + for(auto & level : levels) + std::swap(level.effects, level.cumulativeEffects); } friend class CSpellHandler; friend class Graphics; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index f6ea1eec2..2678bb080 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -4489,30 +4489,32 @@ bool CGameHandler::makeCustomAction(BattleAction &ba) } -void CGameHandler::stackAppearTrigger(const CStack *st) +void CGameHandler::stackEnchantedTrigger(const CStack * st) { auto bl = *(st->getBonuses(Selector::type(Bonus::ENCHANTED))); - for (auto b : bl) + for(auto b : bl) { SetStackEffect sse; int val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); - if (val > 3) + if(val > 3) { - for (auto s : gs->curB->battleGetAllStacks()) + for(auto s : gs->curB->battleGetAllStacks()) { - if (battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied + if(battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied sse.stacks.push_back (s->ID); } } else sse.stacks.push_back (st->ID); - Bonus pseudoBonus; - pseudoBonus.sid = b->subtype; - pseudoBonus.val = ((val > 3) ? (val - 3) : val); - pseudoBonus.turnsRemain = 50; - st->stackEffectToFeature(sse.effect, pseudoBonus); - if (sse.effect.size()) + const CSpell * sp = SpellID(b->subtype).toSpell(); + const int level = ((val > 3) ? (val - 3) : val); + + sp->getEffects(sse.effect, level, false, 50); + //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle + sp->getEffects(sse.cumulativeEffects, level, true, 50); + + if(!sse.effect.empty() || !sse.cumulativeEffects.empty()) sendAndApply(&sse); } } @@ -4526,7 +4528,6 @@ void CGameHandler::stackTurnTrigger(const CStack *st) bte.additionalInfo = 0; if (st->alive()) { - stackAppearTrigger(st); //unbind if (st->hasBonus(Selector::type(Bonus::BIND_EFFECT))) { @@ -5724,7 +5725,7 @@ void CGameHandler::runBattle() } } - stackAppearTrigger(stack); + stackEnchantedTrigger(stack); } //spells opening battle @@ -5766,6 +5767,12 @@ void CGameHandler::runBattle() const BattleInfo & curB = *gs->curB; + for(auto stack : curB.stacks) + { + if(stack->alive() && curB.round > 1) + stackEnchantedTrigger(stack); + } + //stack loop const CStack *next; diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 126f05b7d..6a04c6d50 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -196,7 +196,7 @@ public: bool makeBattleAction(BattleAction &ba); bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) bool makeCustomAction(BattleAction &ba); - void stackAppearTrigger(const CStack *stack); + void stackEnchantedTrigger(const CStack * stack); void stackTurnTrigger(const CStack *stack); void handleDamageFromObstacle(const CObstacleInstance &obstacle, const CStack * curStack); //checks if obstacle is land mine and handles possible consequences void removeObstacle(const CObstacleInstance &obstacle);