1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-24 22:14:36 +02:00
vcmi/lib/spells/effects/Obstacle.cpp
AlexVinS 0b70baa95e Spells configuration version 2 (effect-based)
* Indirect spell effects loading
* Json serializer improvements
* spell->canBeCastAt do not allow useless cast for any spell
* Added proxy caster class for spell-created obstacles
* Handle damage from spell-created obstacles inside mechanics
* Experimental GameState integration/regression tests
* Ignore mod settings and load only "vcmi" mod when running tests
* fixed https://bugs.vcmi.eu/view.php?id=2765 (with tests)
* Huge improvements of BattleAI regarding spell casts
* AI can cast almost any combat spell except TELEPORT, SACRIFICE and obstacle placement spells.
* Possible fix for https://bugs.vcmi.eu/view.php?id=1811
* CStack factored out to several classes
* [Battle] Allowed RETURN_AFTER_STRIKE effect on server side to be optional
* [Battle] Allowed BattleAction have multiple destinations
* [Spells] Converted limit|immunity to target condition
* [Spells] Use partial configuration reload for backward compatibility handling
* [Tests] Started tests for CUnitState
* Partial fixes of fire shield effect
* [Battle] Do HP calculations in 64 bits
* [BattleAI] Use threading for spell cast evaluation
* [BattleAI] Made AI be able to evaluate modified turn order (on hypothetical battle state)
* Implemented https://bugs.vcmi.eu/view.php?id=2811
* plug rare freeze when hypnotized unit shots vertically
* Correctly apply ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT for unit damage, attack & defense
* [BattleAI] Try to not waste a cast if battle is actually won already
* Extended JsonSerializeFormat API
* fixed https://bugs.vcmi.eu/view.php?id=2847
* Any unit effect can be now chained (not only damage like Chain Lightning)
** only damage effect for now actually uses "chainFactor"
* Possible quick fix for https://bugs.vcmi.eu/view.php?id=2860
2018-02-08 11:37:21 +03:00

338 lines
8.3 KiB
C++

/*
* Obstacle.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "Obstacle.h"
#include "Registry.h"
#include "../ISpellMechanics.h"
#include "../../NetPacks.h"
#include "../../battle/IBattleState.h"
#include "../../battle/CBattleInfoCallback.h"
#include "../../serializer/JsonSerializeFormat.h"
static const std::string EFFECT_NAME = "core:obstacle";
namespace spells
{
namespace effects
{
VCMI_REGISTER_SPELL_EFFECT(Obstacle, EFFECT_NAME);
ObstacleSideOptions::ObstacleSideOptions()
: shape(),
range()
{
}
void ObstacleSideOptions::serializeJson(JsonSerializeFormat & handler)
{
serializeRelativeShape(handler, "shape", shape);
serializeRelativeShape(handler, "range", range);
handler.serializeString("appearAnimation", appearAnimation);
handler.serializeString("animation", animation);
handler.serializeInt("offsetY", offsetY);
}
void ObstacleSideOptions::serializeRelativeShape(JsonSerializeFormat & handler, const std::string & fieldName, RelativeShape & value)
{
static const std::vector<std::string> EDirMap =
{
"TL",
"TR",
"R",
"BR",
"BL",
"L",
""
};
{
JsonArraySerializer outer = handler.enterArray(fieldName);
outer.syncSize(value, JsonNode::JsonType::DATA_VECTOR);
for(size_t outerIndex = 0; outerIndex < outer.size(); outerIndex++)
{
JsonArraySerializer inner = outer.enterArray(outerIndex);
inner.syncSize(value.at(outerIndex), JsonNode::JsonType::DATA_STRING);
for(size_t innerIndex = 0; innerIndex < inner.size(); innerIndex++)
{
std::string temp;
if(handler.saving)
{
temp = EDirMap.at(value.at(outerIndex).at(innerIndex));
}
inner.serializeString(innerIndex, temp);
if(!handler.saving)
{
value.at(outerIndex).at(innerIndex) = (BattleHex::EDir) vstd::find_pos(EDirMap, temp);
}
}
}
}
if(!handler.saving)
{
if(value.empty())
value.emplace_back();
if(value.back().empty())
value.back().emplace_back(BattleHex::EDir::NONE);
}
}
Obstacle::Obstacle()
: LocationEffect(),
hidden(false),
passable(false),
trigger(false),
trap(false),
removeOnTrigger(false),
patchCount(1),
turnsRemaining(-1)
{
}
Obstacle::~Obstacle() = default;
void Obstacle::adjustAffectedHexes(std::set<BattleHex> & hexes, const Mechanics * m, const Target & spellTarget) const
{
EffectTarget effectTarget = transformTarget(m, spellTarget, spellTarget);
const ObstacleSideOptions & options = sideOptions.at(m->casterSide);
for(auto & destination : effectTarget)
{
for(auto & trasformation : options.shape)
{
BattleHex hex = destination.hexValue;
for(auto direction : trasformation)
hex.moveInDirection(direction, false);
if(hex.isValid())
hexes.insert(hex);
}
}
}
bool Obstacle::applicable(Problem & problem, const Mechanics * m) const
{
return LocationEffect::applicable(problem, m);
}
bool Obstacle::applicable(Problem & problem, const Mechanics * m, const EffectTarget & target) const
{
if(!m->isMassive())
{
const bool requiresClearTiles = m->requiresClearTiles();
const ObstacleSideOptions & options = sideOptions.at(m->casterSide);
if(target.empty())
return noRoomToPlace(problem, m);
for(const auto & destination : target)
{
for(auto & trasformation : options.shape)
{
BattleHex hex = destination.hexValue;
for(auto direction : trasformation)
hex.moveInDirection(direction, false);
if(!isHexAvailable(m->cb, hex, requiresClearTiles))
return noRoomToPlace(problem, m);
}
}
}
return LocationEffect::applicable(problem, m, target);
}
EffectTarget Obstacle::transformTarget(const Mechanics * m, const Target & aimPoint, const Target & spellTarget) const
{
const ObstacleSideOptions & options = sideOptions.at(m->casterSide);
EffectTarget ret;
if(!m->isMassive())
{
for(auto & spellDestination : spellTarget)
{
for(auto & rangeShape : options.range)
{
BattleHex hex = spellDestination.hexValue;
for(auto direction : rangeShape)
hex.moveInDirection(direction, false);
ret.emplace_back(hex);
}
}
}
return ret;
}
void Obstacle::apply(BattleStateProxy * battleState, RNG & rng, const Mechanics * m, const EffectTarget & target) const
{
if(m->isMassive())
{
std::vector<BattleHex> availableTiles;
for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
{
BattleHex hex = i;
if(isHexAvailable(m->cb, hex, true))
availableTiles.push_back(hex);
}
RandomGeneratorUtil::randomShuffle(availableTiles, rng);
const int patchesToPut = std::min<int>(patchCount, availableTiles.size());
EffectTarget randomTarget;
randomTarget.reserve(patchesToPut);
for(int i = 0; i < patchesToPut; i++)
randomTarget.emplace_back(availableTiles.at(i));
placeObstacles(battleState, m, randomTarget);
}
else
{
placeObstacles(battleState, m, target);
}
}
void Obstacle::serializeJsonEffect(JsonSerializeFormat & handler)
{
handler.serializeBool("hidden", hidden);
handler.serializeBool("passable", passable);
handler.serializeBool("trigger", trigger);
handler.serializeBool("trap", trap);
handler.serializeBool("removeOnTrigger", removeOnTrigger);
handler.serializeInt("patchCount", patchCount);
handler.serializeInt("turnsRemaining", turnsRemaining, -1);
handler.serializeStruct("attacker", sideOptions.at(BattleSide::ATTACKER));
handler.serializeStruct("defender", sideOptions.at(BattleSide::DEFENDER));
}
bool Obstacle::isHexAvailable(const CBattleInfoCallback * cb, const BattleHex & hex, const bool mustBeClear)
{
if(!hex.isAvailable())
return false;
if(!mustBeClear)
return true;
if(cb->battleGetUnitByPos(hex, true))
return false;
auto obst = cb->battleGetAllObstaclesOnPos(hex, false);
for(auto & i : obst)
if(i->obstacleType != CObstacleInstance::MOAT)
return false;
if(cb->battleGetSiegeLevel() != 0)
{
EWallPart::EWallPart part = cb->battleHexToWallPart(hex);
if(part == EWallPart::INVALID || part == EWallPart::INDESTRUCTIBLE_PART_OF_GATE)
return true;//no fortification here
else if(static_cast<int>(part) < 0)
return false;//indestructible 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;
}
bool Obstacle::noRoomToPlace(Problem & problem, const Mechanics * m)
{
MetaString text;
text.addTxt(MetaString::GENERAL_TXT, 181);//No room to place %s here
text.addReplacement(m->getSpellName());
problem.add(std::move(text));
return false;
}
void Obstacle::placeObstacles(BattleStateProxy * battleState, const Mechanics * m, const EffectTarget & target) const
{
const ObstacleSideOptions & options = sideOptions.at(m->casterSide);
BattleObstaclesChanged pack;
auto all = m->cb->battleGetAllObstacles(BattlePerspective::ALL_KNOWING);
int obstacleIdToGive = 1;
for(auto & one : all)
if(one->uniqueID >= obstacleIdToGive)
obstacleIdToGive = one->uniqueID + 1;
for(const Destination & destination : target)
{
SpellCreatedObstacle obstacle;
obstacle.uniqueID = obstacleIdToGive++;
obstacle.pos = destination.hexValue;
obstacle.obstacleType = CObstacleInstance::USUAL;
obstacle.ID = m->getSpellIndex();
obstacle.turnsRemaining = turnsRemaining;
obstacle.casterSpellPower = m->getEffectPower();
obstacle.spellLevel = m->getEffectLevel();//todo: level of indirect effect should be also configurable
obstacle.casterSide = m->casterSide;
obstacle.hidden = hidden;
obstacle.passable = passable;
obstacle.trigger = trigger;
obstacle.trap = trap;
obstacle.removeOnTrigger = removeOnTrigger;
obstacle.appearAnimation = options.appearAnimation;
obstacle.animation = options.animation;
obstacle.animationYOffset = options.offsetY;
obstacle.customSize.clear();
obstacle.customSize.reserve(options.shape.size());
for(auto & shape : options.shape)
{
BattleHex hex = destination.hexValue;
for(auto direction : shape)
hex.moveInDirection(direction, false);
obstacle.customSize.emplace_back(hex);
}
pack.changes.emplace_back();
obstacle.toInfo(pack.changes.back());
}
if(!pack.changes.empty())
battleState->apply(&pack);
}
}
}