mirror of
https://github.com/vcmi/vcmi.git
synced 2024-12-24 22:14:36 +02:00
0b70baa95e
* 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
338 lines
8.3 KiB
C++
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);
|
|
}
|
|
|
|
|
|
}
|
|
}
|