1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-01-02 00:10:22 +02:00

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.
This commit is contained in:
AlexVinS 2016-11-02 20:11:01 +03:00 committed by Arseniy Shestakov
parent 0c7eeeaa5c
commit 0f5202689e
18 changed files with 292 additions and 212 deletions

View File

@ -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);

View File

@ -3,6 +3,9 @@
GENERAL:
* Spectator mode was implemented through command-line options
SPELLS:
* Implemented cumulative effects for spells
0.98 -> 0.99
GENERAL:

View File

@ -1359,7 +1359,7 @@ 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)
@ -1379,11 +1379,9 @@ void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
}
}
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
{
@ -1394,17 +1392,11 @@ CBattleInterface::PossibleActions CBattleInterface::getCasterAction(const CSpell
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;
}

View File

@ -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"
}

View File

@ -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",

View File

@ -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
}

View File

@ -104,21 +104,6 @@ si32 CStack::magicResistance() const
return magicResistance;
}
void CStack::stackEffectToFeature(std::vector<Bonus> & sf, const Bonus & sse)
{
const CSpell * sp = SpellID(sse.sid).toSpell();
std::vector<Bonus> 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) )

View File

@ -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<Bonus> & sf, const Bonus & sse);
std::vector<si32> 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;

View File

@ -1519,11 +1519,19 @@ struct SetStackEffect : public CPackForClient
void applyCl(CClient *cl);
std::vector<ui32> stacks; //affected stacks (IDs)
//regular effects
std::vector<Bonus> effect; //bonuses to apply
std::vector<std::pair<ui32, Bonus> > uniqueBonuses; //bonuses per single stack
//cumulative effects
std::vector<Bonus> cumulativeEffects; //bonuses to apply
std::vector<std::pair<ui32, Bonus> > cumulativeUniqueBonuses; //bonuses per single stack
template <typename Handler> void serialize(Handler &h, const int version)
{
h & stacks & effect & uniqueBonuses;
h & cumulativeEffects & cumulativeUniqueBonuses;
}
};

View File

@ -1554,33 +1554,39 @@ void actualizeEffect(CStack * s, const std::vector<Bonus> & 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<Bonus>(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;
}

View File

@ -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";

View File

@ -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<Bonus> bonuses;
owner->getEffects(bonuses, schoolLevel);
owner->getEffects(bonuses, schoolLevel, false, parameters.caster->getEnchantPower(owner));
for(Bonus b : bonuses)
{

View File

@ -445,8 +445,14 @@ void DefaultSpellMechanics::battleLogDefault(std::vector<MetaString> & logLines,
void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const
{
//applying effects
if(owner->isOffensiveSpell())
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;
@ -469,45 +475,39 @@ void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env,
}
}
if(owner->hasEffects())
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
{
int maxDuration = 0;
std::vector<Bonus> 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
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.back().val = (100 - sse.effect.back().val);
sse.effect.at(sse.effect.size() - 1).val = (100 - sse.effect.back().val);
}
//we need to know who cast Bind
if(owner->id == SpellID::BIND && parameters.casterStack)
else if(owner->id == SpellID::BIND && parameters.casterStack)
{
sse.effect.back().additionalInfo = parameters.casterStack->ID;
sse.effect.at(sse.effect.size() - 1).additionalInfo = parameters.casterStack->ID;
}
std::shared_ptr<Bonus> 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?
si32 power = 0;
for(const CStack * affected : ctx.attackedCres)
{
si32 power = 0;
sse.stacks.push_back(affected->ID);
//Apply hero specials - peculiar enchants
@ -520,20 +520,34 @@ void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env,
{
switch(tier)
{
case 1: case 2:
case 1:
case 2:
power = 3;
break;
case 3: case 4:
case 3:
case 4:
power = 2;
break;
case 5: case 6:
case 5:
case 6:
power = 1;
break;
}
Bonus specialBonus(sse.effect.back());
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<ui32, Bonus>(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<ui32, Bonus>(affected->ID, specialBonus)); //additional premy to given effect
}
}
break;
case 1: //only Coronius as yet
{
@ -557,7 +571,6 @@ void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env,
if(!sse.stacks.empty())
env->sendAndApply(&sse);
}
}
std::vector<BattleHex> DefaultSpellMechanics::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const
{

View File

@ -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 <const CStack*> & reflected) const;

View File

@ -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<Bonus> & lst, const int level) const
void CSpell::getEffects(std::vector<Bonus> & lst, const int level, const bool cumulative, const si32 duration, boost::optional<si32 *> maxDuration/* = boost::none*/) const
{
if(level < 0 || level >= GameConstants::SPELL_SCHOOL_LEVELS)
{
@ -390,19 +390,29 @@ void CSpell::getEffects(std::vector<Bonus> & lst, const int level) const
return;
}
const std::vector<Bonus> & 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();
}

View File

@ -130,16 +130,35 @@ public:
bool clearAffected;
std::string range;
std::vector<Bonus> effects;
std::vector<std::shared_ptr<Bonus>> effectsTmp; //TODO: this should replace effects
std::vector<std::shared_ptr<Bonus>> effects;
std::vector<std::shared_ptr<Bonus>> cumulativeEffects;
LevelInfo();
~LevelInfo();
template <typename Handler> 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<Bonus> old;
h & old;
if(!h.saving)
{
effects.clear();
cumulativeEffects.clear();
for(const Bonus & oldBonus : old)
effects.push_back(std::make_shared<Bonus>(oldBonus));
}
}
h & clearTarget & clearAffected;
}
};
@ -180,7 +199,7 @@ public:
si32 level;
std::map<ESpellSchool, bool> school; //todo: use this instead of separate boolean fields
std::map<ESpellSchool, bool> school;
si32 power; //spell's power
@ -215,8 +234,7 @@ public:
bool isSpecialSpell() const;
bool hasEffects() const;
void getEffects(std::vector<Bonus> &lst, const int level) const;
void getEffects(std::vector<Bonus> & lst, const int level, const bool cumulative, const si32 duration, boost::optional<si32 *> 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;

View File

@ -4489,7 +4489,7 @@ 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)
@ -4507,12 +4507,14 @@ void CGameHandler::stackAppearTrigger(const CStack *st)
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;

View File

@ -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);