1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-24 22:14:36 +02:00

[Spells] More spell related refactoring

+ smart target modifier
- CREATURE_EXPERT_MASSIVE target type
* save format changed

spell format changes already documented in http://wiki.vcmi.eu/index.php?title=Spell_Format
This commit is contained in:
alexvins 2014-03-17 13:11:10 +00:00
parent 7f6f125b4c
commit 9cac0af7be
11 changed files with 806 additions and 551 deletions

View File

@ -1687,13 +1687,13 @@ CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell)
void CMageGuildScreen::Scroll::clickLeft(tribool down, bool previousState)
{
if(down)
LOCPLINT->showInfoDialog(spell->descriptions[0], new CComponent(CComponent::spell,spell->id));
LOCPLINT->showInfoDialog(spell->getLevelInfo(0).description, new CComponent(CComponent::spell,spell->id));
}
void CMageGuildScreen::Scroll::clickRight(tribool down, bool previousState)
{
if(down)
CRClickPopup::createAndPush(spell->descriptions[0], new CComponent(CComponent::spell, spell->id));
CRClickPopup::createAndPush(spell->getLevelInfo(0).description, new CComponent(CComponent::spell, spell->id));
}
void CMageGuildScreen::Scroll::hover(bool on)

View File

@ -630,7 +630,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
|| (!sp->combatSpell && owner->myInt->battleInt))
{
std::vector<CComponent*> hlp(1, new CComponent(CComponent::spell, mySpell, 0));
LOCPLINT->showInfoDialog(sp->descriptions[schoolLevel], hlp);
LOCPLINT->showInfoDialog(sp->getLevelInfo(schoolLevel).description, hlp);
return;
}
@ -810,7 +810,7 @@ void CSpellWindow::SpellArea::clickRight(tribool down, bool previousState)
boost::algorithm::replace_first(dmgInfo, "%d", boost::lexical_cast<std::string>(causedDmg));
}
CRClickPopup::createAndPush(CGI->spellh->objects[mySpell]->descriptions[schoolLevel] + dmgInfo,
CRClickPopup::createAndPush(CGI->spellh->objects[mySpell]->getLevelInfo(schoolLevel).description + dmgInfo,
new CComponent(CComponent::spell, mySpell));
}
}

View File

@ -977,7 +977,7 @@ std::string CComponent::getDescription()
case creature: return "";
case artifact: return CGI->arth->artifacts[subtype]->Description();
case experience: return CGI->generaltexth->allTexts[241];
case spell: return CGI->spellh->objects[subtype]->descriptions[val];
case spell: return CGI->spellh->objects[subtype]->getLevelInfo(val).description;
case morale: return CGI->generaltexth->heroscrn[ 4 - (val>0) + (val<0)];
case luck: return CGI->generaltexth->heroscrn[ 7 - (val>0) + (val<0)];
case building: return CGI->townh->factions[subtype]->town->buildings[BuildingID(val)]->Description();

View File

@ -1549,31 +1549,23 @@ void CBattleInterface::castThisSpell(int spellID)
assert(castingHero); // code below assumes non-null hero
sp = CGI->spellh->objects[spellID];
spellSelMode = ANY_LOCATION;
if(sp->getTargetType() == CSpell::CREATURE)
const CSpell::TargetInfo ti = sp->getTargetInfo(castingHero->getSpellSchoolLevel(sp));
if(ti.massive)
spellSelMode = NO_LOCATION;
else if(ti.type == CSpell::CREATURE)
{
spellSelMode = selectionTypeByPositiveness(*sp);
}
if(sp->getTargetType() == CSpell::CREATURE_EXPERT_MASSIVE)
{
if(castingHero->getSpellSchoolLevel(sp) < 3)
if(ti.smart)
spellSelMode = selectionTypeByPositiveness(*sp);
else
spellSelMode = NO_LOCATION;
}
if(sp->getTargetType() == CSpell::OBSTACLE)
spellSelMode = ANY_CREATURE;
}
else if(ti.type == CSpell::OBSTACLE)
{
spellSelMode = OBSTACLE;
} //FIXME: Remove Obstacle has range X, unfortunatelly :(
else if(sp->range[ castingHero->getSpellSchoolLevel(sp) ] == "X") //spell has no range
{
spellSelMode = NO_LOCATION;
}
if(sp->range[ castingHero->getSpellSchoolLevel(sp) ].size() > 1) //spell has many-hex range
{
spellSelMode = ANY_LOCATION;
}
}
//todo: move to JSON config
if(spellID == SpellID::FIRE_WALL || spellID == SpellID::FORCE_FIELD)
{
spellSelMode = FREE_LOCATION;

View File

@ -1,4 +1,4 @@
{
{
"type":"object",
"$schema": "http://json-schema.org/draft-04/schema",
@ -7,235 +7,241 @@
"description" : "Format used to define new spells in VCMI",
"definitions" : {
"flags" :{
"type" : "object",
"additionalProperties" : {
"type":"boolean"
}
},
"levelInfo":{
"type": "object",
"required":["range","description","cost","power","aiValue","range"],
"definitions" : {
"flags" :{
"type" : "object",
"additionalProperties" : {
"type":"boolean"
}
},
"levelInfo":{
"type": "object",
"required":["range","description","cost","power","aiValue","range"],
"additionalProperties" : false,
"properties":{
"description":{
"type": "string",
"description": "Localizable description. Use {xxx} for formatting"
},
"cost":{
"type": "number",
"description":"Cost in mana points"
},
"power":{
"type": "number",
},
"aiValue":{
"type": "number",
},
"additionalProperties" : false,
"properties":{
"description":{
"type": "string",
"description": "Localizable description. Use {xxx} for formatting"
},
"cost":{
"type": "number",
"description":"Cost in mana points"
},
"power":{
"type": "number",
},
"aiValue":{
"type": "number",
},
"range":{
"type": "string",
"description": "spell range description in SRSL"
},
"effects":{
"type": "object",
"description": "Timed effects",
"additionalProperties" : {
"$ref" : "vcmi:bonus"
}
}
"range":{
"type": "string",
"description": "spell range description in SRSL"
},
"effects":{
"type": "object",
"description": "Timed effects",
"additionalProperties" : {
"$ref" : "vcmi:bonus"
}
},
"targetModifier":{
"type": "object",
"additionalProperties": false,
"properties":{
"smart":{
"type": "boolean",
"description": "true: friendly/hostile based on positiveness; false: all targets"
}
}
}
}
}
},
}
}
},
"required" : ["type", "name", "school", "level", "power","gainChance","flags","levels"],
"additionalProperties" : false,
"required" : ["type", "name", "school", "level", "power","gainChance","flags","levels"],
"additionalProperties" : false,
"properties": {
"index":{
"type": "number",
"description": "numeric id of spell required only for original spells, prohibited for new spells"
},
"type":{
"type": "string",
"enum": ["adventure", "combat", "ability"],
"description":"Spell type"
},
"name":{
"type": "string",
"description": "Localizable name"
"properties": {
"index":{
"type": "number",
"description": "numeric id of spell required only for original spells, prohibited for new spells"
},
"type":{
"type": "string",
"enum": ["adventure", "combat", "ability"],
"description":"Spell type"
},
"name":{
"type": "string",
"description": "Localizable name"
},
"school":{
"type": "object",
"description": "Spell schools",
"additionalProperties": false,
},
"school":{
"type": "object",
"description": "Spell schools",
"additionalProperties": false,
"properties":{
"properties":{
"air":{"type": "boolean"},
"fire":{"type": "boolean"},
"earth":{"type": "boolean"},
"water":{"type": "boolean"}
}
"air":{"type": "boolean"},
"fire":{"type": "boolean"},
"earth":{"type": "boolean"},
"water":{"type": "boolean"}
}
},
"level":{
"type": "number",
"description": "Spell level",
"minimum" : 0,
"maximum" : 5
},
},
"level":{
"type": "number",
"description": "Spell level",
"minimum" : 0,
"maximum" : 5
},
"power":{
"type": "number",
"description": "Base power",
},
"power":{
"type": "number",
"description": "Base power",
},
"defaultGainChance":{
"type": "number",
"description": "Gain chance by default for all factions"
"defaultGainChance":{
"type": "number",
"description": "Gain chance by default for all factions"
},
},
"gainChance":{
"type": "object",
"description": "Chance in % to gain for faction. NOTE: this field is merged with faction config",
"additionalProperties" : {
"type": "number",
"minimum" : 0
}
},
"targetType":{
"type": "string",
"enum": ["NO_TARGET","CREATURE","OBSTACLE"]
},
"anim":{
"type": "number",
"description": "Main effect animation (AC format), -1 - none, deprecated",
"minimum": -1
},
"counters":{
"$ref" : "#/definitions/flags",
"description": "Flags structure ids of countering spells"
},
"flags":{
"type": "object",
"description": "Various flags",
"additionalProperties" : false,
"properties":{
"indifferent": {
"type":"boolean",
"description": "Spell is indifferent for target"
},
"negative": {
"type":"boolean",
"description": "Spell is negative for target"
},
"positive": {
"type":"boolean",
"description": "Spell is positive for target"
},
"damage": {
"type":"boolean",
"description": "Spell does damage (direct or indirect)"
},
"offensive": {
"type":"boolean",
"description": "Spell does direct damage (implicitly sets damage and negative)"
},
"rising":{
"type":"boolean",
"description": "Rising spell (implicitly sets positive)"
},
"special":{
"type": "boolean",
"description": "Special spell. Can be given only by Bonus::SPELL"
}
}
},
"immunity":{
"$ref" : "#/definitions/flags",
"description": "flags structure of bonus names, any one of these bonus grants immunity"
},
"absoluteImmunity":{
"$ref" : "#/definitions/flags",
"description": "flags structure of bonus names. Any one of these bonus grants immunity, cant be negated"
},
"limit":{
"$ref" : "#/definitions/flags",
"description": "flags structure of bonus names, presence of all bonuses required to be affected by"
},
"gainChance":{
"type": "object",
"description": "Chance in % to gain for faction. NOTE: this field is merged with faction config",
"additionalProperties" : {
"type": "number",
"minimum" : 0
}
},
"targetType":{
"type": "string",
"enum": ["NO_TARGET","CREATURE","OBSTACLE","CREATURE_EXPERT_MASSIVE"]
},
"anim":{
"type": "number",
"description": "Main effect animation (AC format), -1 - none, deprecated",
"minimum": -1
},
"counters":{
"$ref" : "#/definitions/flags",
"description": "Flags structure ids of countering spells"
},
"flags":{
"type": "object",
"description": "Various flags",
"additionalProperties" : false,
"properties":{
"indifferent": {
"type":"boolean",
"description": "Spell is indifferent for target"
},
"negative": {
"type":"boolean",
"description": "Spell is negative for target"
},
"positive": {
"type":"boolean",
"description": "Spell is positive for target"
},
"damage": {
"type":"boolean",
"description": "Spell does damage (direct or indirect)"
},
"offensive": {
"type":"boolean",
"description": "Spell does direct damage (implicitly sets damage and negative)"
},
"rising":{
"type":"boolean",
"description": "Rising spell (implicitly sets positive)"
},
"special":{
"type": "boolean",
"description": "Special spell. Can be given only by Bonus::SPELL"
}
}
},
"immunity":{
"$ref" : "#/definitions/flags",
"description": "flags structure of bonus names, any one of these bonus grants immunity"
},
"absoluteImmunity":{
"$ref" : "#/definitions/flags",
"description": "flags structure of bonus names. Any one of these bonus grants immunity, cant be negated"
},
"limit":{
"$ref" : "#/definitions/flags",
"description": "flags structure of bonus names, presence of all bonuses required to be affected by"
},
"graphics":{
"type": "object",
"additionalProperties" : false,
"properties":{
"iconImmune":{
"type": "string",
"description": "Resourse path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES)",
"format" : "imageFile"
},
"iconScenarioBonus":{
"type": "string",
"description": "Resourse path of icon for scenario bonus" ,
"format" : "imageFile"
},
"iconEffect":{
"type": "string",
"description": "Resourse path of icon for spell effects during battle" ,
"format" : "imageFile"
},
"iconBook":{
"type": "string",
"description":"Resourse path of icon for spellbook" ,
"format" : "imageFile"
},
"iconScroll":{
"type": "string",
"description": "Resourse path of icon for spell scrolls",
"format": "imageFile"
}
}
"graphics":{
"type": "object",
"additionalProperties" : false,
"properties":{
"iconImmune":{
"type": "string",
"description": "Resourse path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES)",
"format" : "imageFile"
},
"iconScenarioBonus":{
"type": "string",
"description": "Resourse path of icon for scenario bonus" ,
"format" : "imageFile"
},
"iconEffect":{
"type": "string",
"description": "Resourse path of icon for spell effects during battle" ,
"format" : "imageFile"
},
"iconBook":{
"type": "string",
"description":"Resourse path of icon for spellbook" ,
"format" : "imageFile"
},
"iconScroll":{
"type": "string",
"description": "Resourse path of icon for spell scrolls",
"format": "imageFile"
}
}
},
},
"sounds":{
"type": "object",
"additionalProperties" : false,
"properties":{
"cast":{
"type": "string",
"description": "Resourse path of cast sound"
}
}
},
"sounds":{
"type": "object",
"additionalProperties" : false,
"properties":{
"cast":{
"type": "string",
"description": "Resourse path of cast sound"
}
}
},
"levels":{
"type": "object",
"additionalProperties" : false,
"required" : ["none", "basic", "advanced", "expert"],
"levels":{
"type": "object",
"additionalProperties" : false,
"required" : ["none", "basic", "advanced", "expert"],
"properties":{
"none":{
"$ref" : "#/definitions/levelInfo"
},
"basic":{
"$ref" : "#/definitions/levelInfo"
},
"advanced":{
"$ref" : "#/definitions/levelInfo"
},
"expert":{
"$ref" : "#/definitions/levelInfo"
}
}
}
}
}
"properties":{
"none":{
"$ref" : "#/definitions/levelInfo"
},
"basic":{
"$ref" : "#/definitions/levelInfo"
},
"advanced":{
"$ref" : "#/definitions/levelInfo"
},
"expert":{
"$ref" : "#/definitions/levelInfo"
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1617,10 +1617,13 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleIsImmune(const C
if (spell->isRisingSpell())
{
if (caster) //TODO: what with Archangels?
if(subject->count >= subject->baseAmount)
return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
if (caster) //FIXME: Archangels can cast immune stack
{
auto maxHealth = calculateHealedHP (caster, spell, subject);
if (subject->count >= subject->baseAmount || maxHealth < subject->MaxHealth()) //must be able to rise at least one full creature
if (maxHealth < subject->MaxHealth()) //must be able to rise at least one full creature
return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
}
}
@ -1637,13 +1640,20 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleIsImmune(const C
}
else //no target stack on this tile
{
if(spell->getTargetType() == CSpell::CREATURE
|| (spell->getTargetType() == CSpell::CREATURE_EXPERT_MASSIVE
&& mode == ECastingMode::HERO_CASTING //TODO why???
&& caster
&& caster->getSpellSchoolLevel(spell) < SecSkillLevel::EXPERT))
if(spell->getTargetType() == CSpell::CREATURE)
{
return ESpellCastProblem::WRONG_SPELL_TARGET;
if(caster && mode == ECastingMode::HERO_CASTING) //TODO why???
{
const CSpell::TargetInfo ti = spell->getTargetInfo(caster->getSpellSchoolLevel(spell));
if(!ti.massive)
return ESpellCastProblem::WRONG_SPELL_TARGET;
}
else
{
return ESpellCastProblem::WRONG_SPELL_TARGET;
}
}
}
@ -1688,6 +1698,7 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell
{
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)
{
@ -1726,43 +1737,38 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell
switch(spell->getTargetType())
{
case CSpell::CREATURE:
case CSpell::CREATURE_EXPERT_MASSIVE:
if(mode == ECastingMode::HERO_CASTING)
{
const CGHeroInstance * caster = battleGetFightingHero(side);
const CSpell::TargetInfo ti = spell->getTargetInfo(caster->getSpellSchoolLevel(spell));
bool targetExists = false;
for(const CStack * stack : battleGetAllStacks()) //dead stacks will be immune anyway
{
switch (spell->positiveness)
{
case CSpell::POSITIVE:
if(stack->owner == caster->getOwner())
bool immune = battleIsImmune(caster, spell, mode, stack->position) != ESpellCastProblem::OK;
bool casterStack = stack->owner == caster->getOwner();
if(!immune)
switch (spell->positiveness)
{
if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
case CSpell::POSITIVE:
if(casterStack || !ti.smart)
{
targetExists = true;
break;
}
break;
case CSpell::NEUTRAL:
targetExists = true;
break;
break;
case CSpell::NEGATIVE:
if(!casterStack || !ti.smart)
{
targetExists = true;
break;
}
}
break;
case CSpell::NEUTRAL:
if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
{
targetExists = true;
break;
}
break;
case CSpell::NEGATIVE:
if(stack->owner != caster->getOwner())
{
if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
{
targetExists = true;
break;
}
}
break;
}
}
if(!targetExists)
{
@ -1787,31 +1793,32 @@ std::vector<BattleHex> CBattleInfoCallback::battleGetPossibleTargets(PlayerColor
switch(spell->getTargetType())
{
case CSpell::CREATURE:
case CSpell::CREATURE_EXPERT_MASSIVE:
{
const CGHeroInstance * caster = battleGetFightingHero(playerToSide(player)); //TODO
const CSpell::TargetInfo ti = spell->getTargetInfo(caster->getSpellSchoolLevel(spell));
for(const CStack * stack : battleAliveStacks())
{
switch (spell->positiveness)
{
case CSpell::POSITIVE:
if(stack->owner == caster->getOwner())
if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
bool immune = battleIsImmune(caster, spell, mode, stack->position) != ESpellCastProblem::OK;
bool casterStack = stack->owner == caster->getOwner();
if(!immune)
switch (spell->positiveness)
{
case CSpell::POSITIVE:
if(casterStack || ti.smart)
ret.push_back(stack->position);
break;
break;
case CSpell::NEUTRAL:
if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
case CSpell::NEUTRAL:
ret.push_back(stack->position);
break;
break;
case CSpell::NEGATIVE:
if(stack->owner != caster->getOwner())
if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
case CSpell::NEGATIVE:
if(!casterStack || ti.smart)
ret.push_back(stack->position);
break;
}
break;
}
}
}
break;
@ -1909,7 +1916,7 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell
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 || spell->getTargetType() == CSpell::CREATURE_EXPERT_MASSIVE)
else if(spell->getTargetType() == CSpell::CREATURE)
{
if(!aliveStack)
return ESpellCastProblem::NO_APPROPRIATE_TARGET;
@ -2011,14 +2018,14 @@ std::set<const CStack*> CBattleInfoCallback::getAffectedCreatures(const CSpell *
const auto attackedHexes = spell->rangeInHexes(destinationTile, skillLevel, attackerSide);
const bool onlyAlive = !spell->isRisingSpell(); //when casting resurrection or animate dead we should be allow to select dead stack
const CSpell::TargetInfo ti = spell->getTargetInfo(skillLevel);
//TODO: more generic solution for mass spells
if(spell->id == SpellID::DEATH_RIPPLE || spell->id == SpellID::DESTROY_UNDEAD || spell->id == SpellID::ARMAGEDDON)
if(spell->id == SpellID::DEATH_RIPPLE || spell->id == SpellID::DESTROY_UNDEAD )
{
for(const CStack *stack : battleGetAllStacks())
{
if((spell->id == SpellID::DEATH_RIPPLE && !stack->getCreature()->isUndead()) //death ripple
|| (spell->id == SpellID::DESTROY_UNDEAD && stack->getCreature()->isUndead()) //destroy undead
|| (spell->id == SpellID::ARMAGEDDON) //Armageddon
)
{
if(stack->isValidTarget())
@ -2057,7 +2064,7 @@ std::set<const CStack*> CBattleInfoCallback::getAffectedCreatures(const CSpell *
lightningHex = BattleHex::getClosestTile (stack->attackerOwned, destinationTile, possibleHexes);
}
}
else if (spell->range[skillLevel].size() > 1) //custom many-hex range
else if (spell->getLevelInfo(skillLevel).range.size() > 1) //custom many-hex range
{
for(BattleHex hex : attackedHexes)
{
@ -2075,33 +2082,34 @@ std::set<const CStack*> CBattleInfoCallback::getAffectedCreatures(const CSpell *
}
}
}
else if(spell->getTargetType() == CSpell::CREATURE_EXPERT_MASSIVE)
else if(spell->getTargetType() == CSpell::CREATURE)
{
if(skillLevel < 3) /*not expert */
if (ti.massive)
{
const CStack * st = battleGetStackByPos(destinationTile, onlyAlive);
if(st)
attackedCres.insert(st);
TStacks stacks = battleGetAllStacks();
vstd::erase_if(stacks,[&](const CStack * stack){
return ti.smart && spell->isNegative() && stack->owner == attackerOwner);
});
vstd::erase_if(stacks,[&](const CStack * stack){
return ti.smart && spell->isPositive() && stack->owner != attackerOwner);
});
vstd::erase_if(stacks,[&](const CStack * stack){
return !stack->isValidTarget(!onlyAlive);
});
for (auto stack : stacks)
attackedCres.insert(stack);
}
else
{
for (auto stack : battleGetAllStacks())
{
/*if it's non negative spell and our unit or non positive spell and hostile unit */
if((!spell->isNegative() && stack->owner == attackerOwner)
||(!spell->isPositive() && stack->owner != attackerOwner )
)
{
if(stack->isValidTarget(!onlyAlive))
attackedCres.insert(stack);
}
}
} //if(caster->getSpellSchoolLevel(s) < 3)
}
else if(spell->getTargetType() == CSpell::CREATURE)
{
if(const CStack * st = battleGetStackByPos(destinationTile, onlyAlive))
attackedCres.insert(st);
if(const CStack * st = battleGetStackByPos(destinationTile, onlyAlive))
attackedCres.insert(st);
}
}
else //custom range from attackedHexes
{

View File

@ -23,7 +23,7 @@
namespace SpellConfig
{
static const std::string LEVEL_NAMES[] = {"none", "basic", "advanced", "expert"};
}
using namespace boost::assign;
@ -130,12 +130,21 @@ namespace SRSLPraserHelpers
}
}
using namespace SRSLPraserHelpers;
CSpell::LevelInfo::LevelInfo()
:description(""),cost(0),power(0),AIValue(0),smartTarget(true),range("0")
{
}
CSpell::LevelInfo::~LevelInfo()
{
}
CSpell::CSpell():
id(SpellID::NONE), level(0),
earth(false), water(false), fire(false), air(false),
power(0),
combatSpell(false), creatureAbility(false),
positiveness(ESpellPositiveness::NEUTRAL),
mainEffectAnim(-1),
@ -143,18 +152,29 @@ CSpell::CSpell():
isRising(false), isDamage(false), isOffensive(false),
targetType(ETargetType::NO_TARGET)
{
levels.resize(GameConstants::SPELL_SCHOOL_LEVELS);
}
CSpell::~CSpell()
{
for(auto & elem : effects)
for(size_t j=0; j<elem.size(); j++)
delete elem[j];
}
const CSpell::LevelInfo & CSpell::getLevelInfo(const int level) const
{
if(level < 0 || level >= GameConstants::SPELL_SCHOOL_LEVELS)
{
logGlobal->errorStream() << __FUNCTION__ << " invalid school level " << level;
throw new std::runtime_error("Invalid school level");
}
return levels.at(level);
}
std::vector<BattleHex> CSpell::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const
{
using namespace SRSLPraserHelpers;
std::vector<BattleHex> ret;
if(id == SpellID::FIRE_WALL || id == SpellID::FORCE_FIELD)
@ -192,7 +212,7 @@ std::vector<BattleHex> CSpell::rangeInHexes(BattleHex centralHex, ui8 schoolLvl,
}
std::string rng = range[schoolLvl] + ','; //copy + artificial comma for easier handling
std::string rng = getLevelInfo(schoolLvl).range + ','; //copy + artificial comma for easier handling
if(rng.size() >= 1 && rng[0] != 'X') //there is at lest one hex in range
{
@ -201,7 +221,7 @@ std::vector<BattleHex> CSpell::rangeInHexes(BattleHex centralHex, ui8 schoolLvl,
bool readingFirst = true;
for(auto & elem : rng)
{
if(std::isdigit(elem) ) //reading numer
if(std::isdigit(elem) ) //reading number
{
if(readingFirst)
number1 += elem;
@ -258,6 +278,20 @@ CSpell::ETargetType CSpell::getTargetType() const
return targetType;
}
const CSpell::TargetInfo CSpell::getTargetInfo(const int level) const
{
TargetInfo info;
auto & levelInfo = getLevelInfo(level);
info.type = getTargetType();
info.smart = levelInfo.smartTarget;
info.massive = levelInfo.range == "X";
return info;
}
bool CSpell::isCombatSpell() const
{
return combatSpell;
@ -305,7 +339,7 @@ bool CSpell::isSpecialSpell() const
bool CSpell::hasEffects() const
{
return effects.size() && effects[0].size();
return !levels[0].effects.empty();
}
const std::string& CSpell::getIconImmune() const
@ -322,12 +356,12 @@ const std::string& CSpell::getCastSound() const
si32 CSpell::getCost(const int skillLevel) const
{
return costs[skillLevel];
return getLevelInfo(skillLevel).cost;
}
si32 CSpell::getPower(const int skillLevel) const
{
return powers[skillLevel];
return getLevelInfo(skillLevel).power;
}
//si32 CSpell::calculatePower(const int skillLevel) const
@ -352,27 +386,20 @@ void CSpell::getEffects(std::vector<Bonus>& lst, const int level) const
logGlobal->errorStream() << __FUNCTION__ << " invalid school level " << level;
return;
}
const std::vector<Bonus> & effects = levels[level].effects;
if(effects.empty())
{
logGlobal->errorStream() << __FUNCTION__ << " This spell (" + name + ") has no bonus effects! " << name;
return;
}
if(effects.size() <= level)
{
logGlobal->errorStream() << __FUNCTION__ << " This spell (" + name + ") is missing entry for level " << level;
return;
}
if(effects[level].empty())
{
logGlobal->errorStream() << __FUNCTION__ << " This spell (" + name + ") has no effects for level " << level;
return;
}
lst.reserve(lst.size() + effects[level].size());
lst.reserve(lst.size() + effects.size());
for(Bonus *b : effects[level])
for(const Bonus & b : effects)
{
lst.push_back(Bonus(*b));
lst.push_back(Bonus(b));
}
}
@ -451,20 +478,6 @@ bool CSpell::isImmuneBy(const IBonusBearer* obj) const
return false;
}
void CSpell::setAttributes(const std::string& newValue)
{
attributes = newValue;
if(attributes.find("CREATURE_TARGET_1") != std::string::npos
|| attributes.find("CREATURE_TARGET_2") != std::string::npos)
targetType = CREATURE_EXPERT_MASSIVE;
else if(attributes.find("CREATURE_TARGET") != std::string::npos)
targetType = CREATURE;
else if(attributes.find("OBSTACLE_TARGET") != std::string::npos)
targetType = OBSTACLE;
else
targetType = NO_TARGET;
}
void CSpell::setIsOffensive(const bool val)
{
isOffensive = val;
@ -576,8 +589,6 @@ std::vector<JsonNode> CSpellHandler::loadLegacyData(size_t dataSize)
else if(attributes.find("OBSTACLE_TARGET") != std::string::npos)
targetType = "OBSTACLE";
lineNode["targetType"].String() = targetType;
//save parsed level specific data
for(size_t i = 0; i < GameConstants::SPELL_SCHOOL_LEVELS; i++)
{
@ -588,6 +599,16 @@ std::vector<JsonNode> CSpellHandler::loadLegacyData(size_t dataSize)
level["aiValue"].Float() = AIVals[i];
}
if(targetType == "CREATURE_EXPERT_MASSIVE")
{
lineNode["targetType"].String() = "CREATURE";
getLevel(3)["range"].String() = "X";
}
else
{
lineNode["targetType"].String() = targetType;
}
legacyData.push_back(lineNode);
@ -677,8 +698,6 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json)
spell->targetType = CSpell::CREATURE;
else if(targetType == "OBSTACLE")
spell->targetType = CSpell::OBSTACLE;
else if(targetType == "CREATURE_EXPERT_MASSIVE")
spell->targetType = CSpell::CREATURE_EXPERT_MASSIVE;
spell->mainEffectAnim = json["anim"].Float();
@ -767,17 +786,17 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json)
const JsonNode & graphicsNode = json["graphics"];
spell->iconImmune = graphicsNode["iconImmune"].String();
spell->iconBook = graphicsNode["iconBook"].String();
spell->iconEffect = graphicsNode["iconEffect"].String();
spell->iconScenarioBonus = graphicsNode["iconScenarioBonus"].String();
spell->iconScroll = graphicsNode["iconScroll"].String();
const JsonNode & soundsNode = json["sounds"];
spell->castSound = soundsNode["cast"].String();
@ -785,48 +804,37 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json)
const int levelsCount = GameConstants::SPELL_SCHOOL_LEVELS;
spell->AIVals.resize(levelsCount);
spell->costs.resize(levelsCount);
spell->descriptions.resize(levelsCount);
spell->powers.resize(levelsCount);
spell->range.resize(levelsCount);
for(int levelIndex = 0; levelIndex < levelsCount; levelIndex++)
{
const JsonNode & levelNode = json["levels"][LEVEL_NAMES[levelIndex]];
CSpell::LevelInfo & levelObject = spell->levels[levelIndex];
spell->descriptions[levelIndex] = levelNode["description"].String();
spell->costs[levelIndex] = levelNode["cost"].Float();
spell->powers[levelIndex] = levelNode["power"].Float();
spell->AIVals[levelIndex] = levelNode["aiValue"].Float();
const JsonNode & effectsNode = levelNode["effects"];
if(!effectsNode.isNull())
const si32 levelPower = levelNode["power"].Float();
levelObject.description = levelNode["description"].String();
levelObject.cost = levelNode["cost"].Float();
levelObject.power = levelPower;
levelObject.AIValue = levelNode["aiValue"].Float();
levelObject.smartTarget = levelNode["targetModifier"]["smart"].Bool();
for(const auto & elem : levelNode["effects"].Struct())
{
if(spell->effects.empty())
spell->effects.resize(levelsCount);
const JsonNode & bonusNode = elem.second;
Bonus * b = JsonUtils::parseBonus(bonusNode);
const bool usePowerAsValue = bonusNode["val"].isNull();
for(const auto & elem : effectsNode.Struct())
{
const JsonNode & bonusNode = elem.second;
Bonus * b = JsonUtils::parseBonus(bonusNode);
const bool usePowerAsValue = bonusNode["val"].isNull();
//TODO: make this work. see CSpellHandler::afterLoadFinalization()
//b->sid = spell->id; //for all
//TODO: make this work. see CSpellHandler::afterLoadFinalization()
//b->sid = spell->id; //for all
b->source = Bonus::SPELL_EFFECT;//for all
b->source = Bonus::SPELL_EFFECT;//for all
if(usePowerAsValue)
{
b->val = spell->powers[levelIndex];
}
if(usePowerAsValue)
b->val = levelPower;
spell->effects[levelIndex].push_back(b);
}
levelObject.effects.push_back(*b);
}
}
return spell;
@ -836,9 +844,9 @@ void CSpellHandler::afterLoadFinalization()
{
//FIXME: it is a bad place for this code, should refactor loadFromJson to know object id during loading
for(auto spell: objects)
for(auto & level: spell->effects)
for(auto * bonus: level)
bonus->sid = spell->id;
for(auto & level: spell->levels)
for(auto & bonus: level.effects)
bonus.sid = spell->id;
}

View File

@ -23,26 +23,49 @@ struct BattleHex;
class DLL_LINKAGE CSpell
{
public:
// struct LevelInfo
// {
//
// };
//
// /** \brief Low level accessor. Don`t use it if absolutely necessary
// *
// * \param level. spell school level
// * \return Spell level info structure
// *
// */
// const LevelInfo& getLevelInfo(const int level);
struct LevelInfo
{
std::string description; //descriptions of spell for skill level
si32 cost; //per skill level: 0 - none, 1 - basic, etc
si32 power; //per skill level: 0 - none, 1 - basic, etc
si32 AIValue; //AI values: per skill level: 0 - none, 1 - basic, etc
bool smartTarget;
std::string range;
std::vector<Bonus> effects;
LevelInfo();
~LevelInfo();
template <typename Handler> void serialize(Handler &h, const int version)
{
h & description & cost & power & AIValue & smartTarget & range & effects;
}
};
/** \brief Low level accessor. Don`t use it if absolutely necessary
*
* \param level. spell school level
* \return Spell level info structure
*
*/
const CSpell::LevelInfo& getLevelInfo(const int level) const;
public:
enum ETargetType {NO_TARGET, CREATURE, CREATURE_EXPERT_MASSIVE, OBSTACLE};
enum ETargetType {NO_TARGET, CREATURE, OBSTACLE};
enum ESpellPositiveness {NEGATIVE = -1, NEUTRAL = 0, POSITIVE = 1};
struct TargetInfo
{
ETargetType type;
bool smart;
bool massive;
};
SpellID id;
std::string identifier; //???
std::string name;
std::string abbName; //abbreviated name
std::vector<std::string> descriptions; //descriptions of spell for skill levels: 0 - none, 1 - basic, etc
si32 level;
bool earth;
bool water;
@ -55,7 +78,7 @@ public:
bool combatSpell; //is this spell combat (true) or adventure (false)
bool creatureAbility; //if true, only creatures can use this spell
si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative
std::vector<std::string> range; //description of spell's range in SRSL by magic school level
std::vector<SpellID> counteredSpells; //spells that are removed when effect of this spell is placed on creature (for bless-curse, haste-slow, and similar pairs)
CSpell();
@ -63,7 +86,9 @@ public:
std::vector<BattleHex> rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes = nullptr ) const; //convert range to specific hexes; last optional out parameter is set to true, if spell would cover unavailable hexes (that are not included in ret)
si16 mainEffectAnim; //main spell effect animation, in AC format (or -1 when none)
ETargetType getTargetType() const;
ETargetType getTargetType() const; //deprecated
const CSpell::TargetInfo getTargetInfo(const int level) const;
bool isCombatSpell() const;
bool isAdventureSpell() const;
@ -102,23 +127,25 @@ public:
* Returns resource name of icon for SPELL_IMMUNITY bonus
*/
const std::string& getIconImmune() const;
const std::string& getCastSound() const;
template <typename Handler> void serialize(Handler &h, const int version)
{
h & identifier & id & name & abbName & descriptions & level & earth & water & fire & air & power & costs
& powers & probabilities & AIVals & attributes & combatSpell & creatureAbility & positiveness & range & counteredSpells & mainEffectAnim;
h & identifier & id & name & level & earth & water & fire & air & power
& probabilities & attributes & combatSpell & creatureAbility & positiveness & counteredSpells & mainEffectAnim;
h & isRising & isDamage & isOffensive;
h & targetType;
h & effects & immunities & limiters;
h & immunities & limiters;
h & iconImmune;
h & absoluteImmunities & defaultProbability;
h & isSpecial;
h & castSound & iconBook & iconEffect & iconScenarioBonus & iconScroll ;
h & castSound & iconBook & iconEffect & iconScenarioBonus & iconScroll;
h & levels;
}
friend class CSpellHandler;
friend class Graphics;
@ -126,13 +153,9 @@ public:
private:
void setIsOffensive(const bool val);
void setIsRising(const bool val);
void setAttributes(const std::string& newValue);
private:
si32 defaultProbability;
std::vector<si32> costs; //per skill level: 0 - none, 1 - basic, etc
std::vector<si32> powers; //per skill level: 0 - none, 1 - basic, etc
std::vector<si32> AIVals; //AI values: per skill level: 0 - none, 1 - basic, etc
bool isRising;
bool isDamage;
@ -141,11 +164,8 @@ private:
std::string attributes; //reference only attributes //todo: remove or include in configuration format, currently unused
ETargetType targetType;
std::vector<std::vector<Bonus *> > effects; // [level 0-3][list of effects]
std::vector<Bonus::BonusType> immunities; //any of these grants immunity
std::vector<Bonus::BonusType> absoluteImmunities; //any of these grants immunity, cant be negated
std::vector<Bonus::BonusType> limiters; //all of them are required to be affected
@ -153,14 +173,16 @@ private:
///graphics related stuff
std::string iconImmune;
std::string iconBook;
std::string iconEffect;
std::string iconScenarioBonus;
std::string iconScenarioBonus;
std::string iconScroll;
///sound related stuff
std::string castSound;
std::vector<LevelInfo> levels;
};

View File

@ -28,7 +28,7 @@
#include "mapping/CCampaignHandler.h" //for CCampaignState
#include "rmg/CMapGenerator.h" // for CMapGenOptions
const ui32 version = 747;
const ui32 version = 748;
const ui32 minSupportedVersion = version;
class CConnection;

View File

@ -4082,50 +4082,50 @@ void CGameHandler::handleSpellCasting( SpellID spellID, int spellLvl, BattleHex
if (spell->isOffensiveSpell())
{
int spellDamage = 0;
if (stack && mode != ECastingMode::MAGIC_MIRROR)
int spellDamage = 0;
if (stack && mode != ECastingMode::MAGIC_MIRROR)
{
int unitSpellPower = stack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spellID.toEnum());
if (unitSpellPower)
sc.dmgToDisplay = spellDamage = stack->count * unitSpellPower; //TODO: handle immunities
else //Faerie Dragon
{
int unitSpellPower = stack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spellID.toEnum());
if (unitSpellPower)
sc.dmgToDisplay = spellDamage = stack->count * unitSpellPower; //TODO: handle immunities
else //Faerie Dragon
{
usedSpellPower = stack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * stack->count / 100;
sc.dmgToDisplay = 0;
}
usedSpellPower = stack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * stack->count / 100;
sc.dmgToDisplay = 0;
}
int chainLightningModifier = 0;
for(auto & attackedCre : attackedCres)
}
int chainLightningModifier = 0;
for(auto & attackedCre : attackedCres)
{
if(vstd::contains(sc.resisted, (attackedCre)->ID)) //this creature resisted the spell
continue;
BattleStackAttacked bsa;
if ((destination > -1 && (attackedCre)->coversPos(destination)) || (spell->getLevelInfo(spellLvl).range == "X" || mode == ECastingMode::ENCHANTER_CASTING))
//display effect only upon primary target of area spell
//FIXME: if no stack is attacked, there is no animation and interface freezes
{
if(vstd::contains(sc.resisted, (attackedCre)->ID)) //this creature resisted the spell
continue;
BattleStackAttacked bsa;
if ((destination > -1 && (attackedCre)->coversPos(destination)) || (spell->range.at(spellLvl) == "X" || mode == ECastingMode::ENCHANTER_CASTING))
//display effect only upon primary target of area spell
//FIXME: if no stack is attacked, ther eis no animation and interface freezes
{
bsa.flags |= BattleStackAttacked::EFFECT;
bsa.effect = spell->mainEffectAnim;
}
if (spellDamage)
bsa.damageAmount = spellDamage >> chainLightningModifier;
else
bsa.damageAmount = gs->curB->calculateSpellDmg(spell, caster, attackedCre, spellLvl, usedSpellPower) >> chainLightningModifier;
sc.dmgToDisplay += bsa.damageAmount;
bsa.stackAttacked = (attackedCre)->ID;
if (mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast
bsa.attackerID = stack->ID;
else
bsa.attackerID = -1;
(attackedCre)->prepareAttacked(bsa);
si.stacks.push_back(bsa);
if (spellID == SpellID::CHAIN_LIGHTNING)
++chainLightningModifier;
bsa.flags |= BattleStackAttacked::EFFECT;
bsa.effect = spell->mainEffectAnim;
}
if (spellDamage)
bsa.damageAmount = spellDamage >> chainLightningModifier;
else
bsa.damageAmount = gs->curB->calculateSpellDmg(spell, caster, attackedCre, spellLvl, usedSpellPower) >> chainLightningModifier;
sc.dmgToDisplay += bsa.damageAmount;
bsa.stackAttacked = (attackedCre)->ID;
if (mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast
bsa.attackerID = stack->ID;
else
bsa.attackerID = -1;
(attackedCre)->prepareAttacked(bsa);
si.stacks.push_back(bsa);
if (spellID == SpellID::CHAIN_LIGHTNING)
++chainLightningModifier;
}
}
if (spell->hasEffects())
@ -4456,7 +4456,7 @@ void CGameHandler::handleSpellCasting( SpellID spellID, int spellLvl, BattleHex
}
//Magic Mirror effect
if (spell->isNegative() && mode != ECastingMode::MAGIC_MIRROR && spell->level && spell->range.at(0) == "0") //it is actual spell and can be reflected to single target, no recurrence
if (spell->isNegative() && mode != ECastingMode::MAGIC_MIRROR && spell->level && spell->getLevelInfo(0).range == "0") //it is actual spell and can be reflected to single target, no recurrence
{
for(auto & attackedCre : attackedCres)
{
@ -5354,7 +5354,7 @@ void CGameHandler::attackCasting(const BattleAttack & bat, Bonus::BonusType atta
void CGameHandler::handleAttackBeforeCasting (const BattleAttack & bat)
{
const CStack * attacker = gs->curB->battleGetStackByID(bat.stackAttacking);
attackCasting(bat, Bonus::SPELL_BEFORE_ATTACK, attacker); //no detah stare / acid bretah needed?
attackCasting(bat, Bonus::SPELL_BEFORE_ATTACK, attacker); //no death stare / acid breath needed?
}
void CGameHandler::handleAfterAttackCasting( const BattleAttack & bat )