1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-25 22:42:04 +02:00

Merge branch 'develop' into generate_overlay_shadow

This commit is contained in:
Laserlicht
2025-07-14 23:24:23 +02:00
committed by GitHub
72 changed files with 2011 additions and 1106 deletions

View File

@@ -12,6 +12,8 @@
#include "../Engine/Nullkiller.h"
#include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/IGameSettings.h"
#include "../../../lib/spells/ISpellMechanics.h"
#include "../../../lib/spells/adventure/TownPortalEffect.h"
namespace NKAI
{
@@ -210,32 +212,31 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
{
auto hasFly = hero->spellbookContainsSpell(SpellID::FLY);
auto hasTownPortal = hero->spellbookContainsSpell(SpellID::TOWN_PORTAL);
auto manaLimit = hero->manaLimit();
auto spellPower = hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER);
auto hasEarth = hero->getSpellSchoolLevel(SpellID(SpellID::TOWN_PORTAL).toSpell()) > 0;
auto score = 0.0f;
// FIXME: this will not cover spells give by scrolls / tomes. Intended?
for(auto spellId : hero->getSpellsInSpellbook())
{
auto spell = spellId.toSpell();
auto schoolLevel = hero->getSpellSchoolLevel(spell);
auto townPortalEffect = spell->getAdventureMechanics().getEffectAs<TownPortalEffect>(hero);
score += (spell->getLevel() + 1) * (schoolLevel + 1) * 0.05f;
if (spell->getAdventureMechanics().givesBonus(hero, BonusType::FLYING_MOVEMENT))
score += 0.3;
if(townPortalEffect != nullptr && schoolLevel != 0)
score += 0.6f;
}
vstd::amin(score, 1);
score *= std::min(1.0f, spellPower / 10.0f);
if(hasFly)
score += 0.3f;
if(hasTownPortal && hasEarth)
score += 0.6f;
vstd::amin(score, 1);
score *= std::min(1.0f, manaLimit / 100.0f);

View File

@@ -10,6 +10,8 @@
#include "StdInc.h"
#include "AdventureSpellCast.h"
#include "../AIGateway.h"
#include "../../../lib/spells/ISpellMechanics.h"
#include "../../../lib/spells/adventure/TownPortalEffect.h"
namespace NKAI
{
@@ -39,8 +41,9 @@ void AdventureSpellCast::accept(AIGateway * ai)
if(hero->mana < hero->getSpellCost(spell))
throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->getNameTranslated());
auto townPortalEffect = spell->getAdventureMechanics().getEffectAs<TownPortalEffect>(hero);
if(town && spellID == SpellID::TOWN_PORTAL)
if(town && townPortalEffect)
{
ai->selectedObject = town->id;
@@ -61,7 +64,7 @@ void AdventureSpellCast::accept(AIGateway * ai)
cb->waitTillRealize = true;
cb->castSpell(hero, spellID, tile);
if(town && spellID == SpellID::TOWN_PORTAL)
if(town && townPortalEffect)
{
// visit town
ai->moveHeroToTile(town->visitablePos(), hero);

View File

@@ -17,6 +17,8 @@
#include "../../../lib/pathfinder/CPathfinder.h"
#include "../../../lib/pathfinder/PathfinderUtil.h"
#include "../../../lib/pathfinder/PathfinderOptions.h"
#include "../../../lib/spells/ISpellMechanics.h"
#include "../../../lib/spells/adventure/TownPortalEffect.h"
#include "../../../lib/IGameSettings.h"
#include "../../../lib/CPlayerState.h"
@@ -1059,32 +1061,27 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
struct TownPortalFinder
{
const std::vector<CGPathNode *> & initialNodes;
MasteryLevel::Type townPortalSkillLevel;
uint64_t movementNeeded;
const ChainActor * actor;
const CGHeroInstance * hero;
std::vector<const CGTownInstance *> targetTowns;
AINodeStorage * nodeStorage;
SpellID spellID;
const CSpell * townPortal;
uint64_t movementNeeded;
SpellID spellID;
bool townSelectionAllowed;
TownPortalFinder(
const ChainActor * actor,
const std::vector<CGPathNode *> & initialNodes,
std::vector<const CGTownInstance *> targetTowns,
AINodeStorage * nodeStorage)
:actor(actor), initialNodes(initialNodes), hero(actor->hero),
targetTowns(targetTowns), nodeStorage(nodeStorage)
TownPortalFinder(const ChainActor * actor, const std::vector<CGPathNode *> & initialNodes, const std::vector<const CGTownInstance *> & targetTowns, AINodeStorage * nodeStorage, SpellID spellID)
: initialNodes(initialNodes)
, actor(actor)
, hero(actor->hero)
, targetTowns(targetTowns)
, nodeStorage(nodeStorage)
, townPortal(spellID.toSpell())
, spellID(spellID)
{
spellID = SpellID::TOWN_PORTAL;
townPortal = spellID.toSpell();
// TODO: Copy/Paste from TownPortalMechanics
townPortalSkillLevel = MasteryLevel::Type(hero->getSpellSchoolLevel(townPortal));
int baseCost = hero->cb->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
movementNeeded = baseCost * (townPortalSkillLevel >= MasteryLevel::EXPERT ? 2 : 3);
auto townPortalEffect = townPortal->getAdventureMechanics().getEffectAs<TownPortalEffect>(hero);
movementNeeded = townPortalEffect->getMovementPointsRequired();
townSelectionAllowed = townPortalEffect->townSelectionAllowed();
}
bool actorCanCastTownPortal()
@@ -1105,7 +1102,7 @@ struct TownPortalFinder
continue;
}
if(townPortalSkillLevel < MasteryLevel::ADVANCED)
if(!townSelectionAllowed)
{
const CGTownInstance * nearestTown = *vstd::minElementByFun(targetTowns, [&](const CGTownInstance * t) -> int
{
@@ -1151,7 +1148,7 @@ struct TownPortalFinder
DO_NOT_SAVE_TO_COMMITTED_TILES);
node->theNodeBefore = bestNode;
node->addSpecialAction(std::make_shared<AIPathfinding::TownPortalAction>(targetTown));
node->addSpecialAction(std::make_shared<AIPathfinding::TownPortalAction>(targetTown, spellID));
}
return nodeOptional;
@@ -1177,10 +1174,21 @@ void AINodeStorage::calculateTownPortal(
return; // no towns no need to run loop further
}
TownPortalFinder townPortalFinder(actor, initialNodes, towns, this);
if(townPortalFinder.actorCanCastTownPortal())
for (const auto & spell : LIBRARY->spellh->objects)
{
if (!spell || !spell->isAdventure())
continue;
auto townPortalEffect = spell->getAdventureMechanics().getEffectAs<TownPortalEffect>(actor->hero);
if (!townPortalEffect)
continue;
TownPortalFinder townPortalFinder(actor, initialNodes, towns, this, spell->id);
if(!townPortalFinder.actorCanCastTownPortal())
continue;
for(const CGTownInstance * targetTown : towns)
{
if(targetTown->getVisitingHero()

View File

@@ -28,12 +28,12 @@ namespace AIPathfinding
manaCost = hero->getSpellCost(spellToCast.toSpell());
}
WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero)
:AdventureCastAction(SpellID::WATER_WALK, hero, DayFlags::WATER_WALK_CAST)
WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero, SpellID spellToCast)
:AdventureCastAction(spellToCast, hero, DayFlags::WATER_WALK_CAST)
{ }
AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero)
: AdventureCastAction(SpellID::FLY, hero, DayFlags::FLY_CAST)
AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero, SpellID spellToCast)
: AdventureCastAction(spellToCast, hero, DayFlags::FLY_CAST)
{
}

View File

@@ -45,14 +45,16 @@ namespace AIPathfinding
class WaterWalkingAction : public AdventureCastAction
{
SpellID spellToCast;
public:
WaterWalkingAction(const CGHeroInstance * hero);
WaterWalkingAction(const CGHeroInstance * hero, SpellID spellToCast);
};
class AirWalkingAction : public AdventureCastAction
{
SpellID spellToCast;
public:
AirWalkingAction(const CGHeroInstance * hero);
AirWalkingAction(const CGHeroInstance * hero, SpellID spellToCast);
};
}

View File

@@ -92,7 +92,7 @@ namespace AIPathfinding
void SummonBoatAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
{
Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai);
Goals::AdventureSpellCast(hero, usedSpell).accept(ai);
}
const ChainActor * SummonBoatAction::getActor(const ChainActor * sourceActor) const
@@ -139,10 +139,8 @@ namespace AIPathfinding
int32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const
{
SpellID summonBoat = SpellID::SUMMON_BOAT;
// FIXME: this should be hero->getSpellCost, however currently queries to bonus system are too slow
return summonBoat.toSpell()->getCost(0);
return usedSpell.toSpell()->getCost(0);
}
}

View File

@@ -24,7 +24,13 @@ namespace AIPathfinding
class SummonBoatAction : public VirtualBoatAction
{
SpellID usedSpell;
public:
SummonBoatAction(SpellID usedSpell)
: usedSpell(usedSpell)
{
}
void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
virtual void applyOnDestination(

View File

@@ -20,7 +20,7 @@ using namespace AIPathfinding;
void TownPortalAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
{
auto goal = Goals::AdventureSpellCast(hero, SpellID::TOWN_PORTAL);
auto goal = Goals::AdventureSpellCast(hero, usedSpell);
goal.town = target;
goal.tile = target->visitablePos();

View File

@@ -22,10 +22,12 @@ namespace AIPathfinding
{
private:
const CGTownInstance * target;
SpellID usedSpell;
public:
TownPortalAction(const CGTownInstance * target)
TownPortalAction(const CGTownInstance * target, SpellID usedSpell)
:target(target)
,usedSpell(usedSpell)
{
}

View File

@@ -12,6 +12,8 @@
#include "../../Engine/Nullkiller.h"
#include "../../../../lib/pathfinder/CPathfinder.h"
#include "../../../../lib/pathfinder/TurnInfo.h"
#include "../../../../lib/spells/ISpellMechanics.h"
#include "../../../../lib/spells/adventure/SummonBoatEffect.h"
namespace NKAI
{
@@ -109,19 +111,22 @@ namespace AIPathfinding
void AILayerTransitionRule::setup()
{
SpellID waterWalk = SpellID::WATER_WALK;
SpellID airWalk = SpellID::FLY;
for(const CGHeroInstance * hero : nodeStorage->getAllHeroes())
{
if(hero->canCastThisSpell(waterWalk.toSpell()) && hero->mana >= hero->getSpellCost(waterWalk.toSpell()))
for (const auto & spell : LIBRARY->spellh->objects)
{
waterWalkingActions[hero] = std::make_shared<WaterWalkingAction>(hero);
}
if (!spell || !spell->isAdventure())
continue;
if(hero->canCastThisSpell(airWalk.toSpell()) && hero->mana >= hero->getSpellCost(airWalk.toSpell()))
{
airWalkingActions[hero] = std::make_shared<AirWalkingAction>(hero);
if(spell->getAdventureMechanics().givesBonus(hero, BonusType::WATER_WALKING) && hero->canCastThisSpell(spell.get()) && hero->mana >= hero->getSpellCost(spell.get()))
{
waterWalkingActions[hero] = std::make_shared<WaterWalkingAction>(hero, spell->id);
}
if(spell->getAdventureMechanics().givesBonus(hero, BonusType::FLYING_MOVEMENT) && hero->canCastThisSpell(spell.get()) && hero->mana >= hero->getSpellCost(spell.get()))
{
airWalkingActions[hero] = std::make_shared<AirWalkingAction>(hero, spell->id);
}
}
}
@@ -159,13 +164,21 @@ namespace AIPathfinding
for(const CGHeroInstance * hero : nodeStorage->getAllHeroes())
{
auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell();
if(hero->canCastThisSpell(summonBoatSpell)
&& hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED)
for (const auto & spell : LIBRARY->spellh->objects)
{
// TODO: For lower school level we might need to check the existence of some boat
summonableVirtualBoats[hero] = std::make_shared<SummonBoatAction>();
if (!spell || !spell->isAdventure())
continue;
auto effect = spell->getAdventureMechanics().getEffectAs<SummonBoatEffect>(hero);
if (!effect || !hero->canCastThisSpell(spell.get()))
continue;
if (effect->canCreateNewBoat() && effect->getSuccessChance(hero) == 100)
{
// TODO: For lower school level we might need to check the existence of some boat
summonableVirtualBoats[hero] = std::make_shared<SummonBoatAction>(spell->id);
}
}
}
}

View File

@@ -13,6 +13,8 @@
#include "../FuzzyHelper.h"
#include "../AIhelper.h"
#include "../../../lib/mapObjects/CGTownInstance.h"
#include "../../../lib/spells/ISpellMechanics.h"
#include "../../../lib/spells/adventure/TownPortalEffect.h"
using namespace Goals;
@@ -39,7 +41,9 @@ TSubgoal AdventureSpellCast::whatToDoToAchieve()
if(hero->mana < hero->getSpellCost(spell))
throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->getNameTranslated());
if(spellID == SpellID::TOWN_PORTAL && town && town->getVisitingHero())
auto townPortalEffect = spell->getAdventureMechanics().getEffectAs<TownPortalEffect>(hero.h);
if(townPortalEffect && town && town->getVisitingHero())
throw cannotFulfillGoalException("The town is already occupied by " + town->getVisitingHero()->getNameTranslated());
return iAmElementar();
@@ -47,7 +51,9 @@ TSubgoal AdventureSpellCast::whatToDoToAchieve()
void AdventureSpellCast::accept(VCAI * ai)
{
if(town && spellID == SpellID::TOWN_PORTAL)
auto townPortalEffect = spellID.toSpell()->getAdventureMechanics().getEffectAs<TownPortalEffect>(hero.h);
if(town && townPortalEffect)
{
ai->selectedObject = town->id;
}
@@ -57,7 +63,7 @@ void AdventureSpellCast::accept(VCAI * ai)
cb->waitTillRealize = true;
cb->castSpell(hero.h, spellID, tile);
if(town && spellID == SpellID::TOWN_PORTAL)
if(town && townPortalEffect)
{
// visit town
ai->moveHeroToTile(town->visitablePos(), hero);

View File

@@ -15,6 +15,8 @@
#include "../../../lib/pathfinder/CPathfinder.h"
#include "../../../lib/pathfinder/PathfinderOptions.h"
#include "../../../lib/pathfinder/PathfinderUtil.h"
#include "../../../lib/spells/ISpellMechanics.h"
#include "../../../lib/spells/adventure/TownPortalEffect.h"
#include "../../../lib/IGameSettings.h"
#include "../../../lib/CPlayerState.h"
@@ -225,12 +227,21 @@ void AINodeStorage::calculateTownPortalTeleportations(
const PathNodeInfo & source,
std::vector<CGPathNode *> & neighbours)
{
SpellID spellID = SpellID::TOWN_PORTAL;
const CSpell * townPortal = spellID.toSpell();
auto srcNode = getAINode(source.node);
if(hero->canCastThisSpell(townPortal) && hero->mana >= hero->getSpellCost(townPortal))
for (const auto & spell : LIBRARY->spellh->objects)
{
if (!spell || !spell->isAdventure())
continue;
auto townPortalEffect = spell->getAdventureMechanics().getEffectAs<TownPortalEffect>(hero);
if (!townPortalEffect)
continue;
if(!hero->canCastThisSpell(spell.get()) || hero->mana < hero->getSpellCost(spell.get()))
continue;
auto towns = cb->getTownsInfo(false);
vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool
@@ -238,22 +249,15 @@ void AINodeStorage::calculateTownPortalTeleportations(
return cb->getPlayerRelations(hero->tempOwner, t->tempOwner) == PlayerRelations::ENEMIES;
});
if(!towns.size())
if(towns.empty())
return;
if(hero->movementPointsRemaining() < townPortalEffect->getMovementPointsRequired())
{
return;
}
// TODO: Copy/Paste from TownPortalMechanics
auto skillLevel = hero->getSpellSchoolLevel(townPortal);
int baseCost = hero->cb->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
auto movementCost = baseCost * (skillLevel >= 3 ? 2 : 3);
if(hero->movementPointsRemaining() < movementCost)
{
return;
}
if(skillLevel < MasteryLevel::ADVANCED)
if(!townPortalEffect->townSelectionAllowed())
{
const CGTownInstance * nearestTown = *vstd::minElementByFun(towns, [&](const CGTownInstance * t) -> int
{
@@ -279,7 +283,7 @@ void AINodeStorage::calculateTownPortalTeleportations(
AIPathNode * node = nodeOptional.value();
node->theNodeBefore = source.node;
node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown));
node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown, spell->id));
node->moveRemains = source.node->moveRemains;
neighbours.push_back(node);

View File

@@ -23,7 +23,7 @@ namespace AIPathfinding
Goals::TSubgoal SummonBoatAction::whatToDo(const HeroPtr & hero) const
{
return Goals::sptr(Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT));
return Goals::sptr(Goals::AdventureSpellCast(hero, usedSpell));
}
void SummonBoatAction::applyOnDestination(
@@ -53,8 +53,6 @@ namespace AIPathfinding
uint32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const
{
SpellID summonBoat = SpellID::SUMMON_BOAT;
return hero->getSpellCost(summonBoat.toSpell());
return hero->getSpellCost(usedSpell.toSpell());
}
}

View File

@@ -34,9 +34,11 @@ namespace AIPathfinding
class SummonBoatAction : public VirtualBoatAction
{
SpellID usedSpell;
public:
SummonBoatAction()
SummonBoatAction(SpellID usedSpell)
:VirtualBoatAction(AINodeStorage::CAST_CHAIN)
,usedSpell(usedSpell)
{
}

View File

@@ -19,5 +19,5 @@ Goals::TSubgoal TownPortalAction::whatToDo(const HeroPtr & hero) const
{
const CGTownInstance * targetTown = target; // const pointer is not allowed in settown
return Goals::sptr(Goals::AdventureSpellCast(hero, SpellID::TOWN_PORTAL).settown(targetTown).settile(targetTown->visitablePos()));
return Goals::sptr(Goals::AdventureSpellCast(hero, spellToUse).settown(targetTown).settile(targetTown->visitablePos()));
}

View File

@@ -20,10 +20,12 @@ namespace AIPathfinding
{
private:
const CGTownInstance * target;
SpellID spellToUse;
public:
TownPortalAction(const CGTownInstance * target)
TownPortalAction(const CGTownInstance * target, SpellID spellToUse)
:target(target)
,spellToUse(spellToUse)
{
}

View File

@@ -10,6 +10,9 @@
#include "StdInc.h"
#include "AILayerTransitionRule.h"
#include "../../../../lib/spells/ISpellMechanics.h"
#include "../../../../lib/spells/adventure/SummonBoatEffect.h"
namespace AIPathfinding
{
AILayerTransitionRule::AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr<AINodeStorage> nodeStorage)
@@ -74,13 +77,22 @@ namespace AIPathfinding
}
auto hero = nodeStorage->getHero();
auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell();
if(hero->canCastThisSpell(summonBoatSpell)
&& hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED)
for (const auto & spell : LIBRARY->spellh->objects)
{
// TODO: For lower school level we might need to check the existence of some boat
summonableVirtualBoat.reset(new SummonBoatAction());
if (!spell || !spell->isAdventure())
continue;
auto effect = spell->getAdventureMechanics().getEffectAs<SummonBoatEffect>(hero);
if (!effect || !hero->canCastThisSpell(spell.get()))
continue;
if (effect->canCreateNewBoat() && effect->getSuccessChance(hero) == 100)
{
// TODO: For lower school level we might need to check the existence of some boat
summonableVirtualBoat.reset(new SummonBoatAction(spell->id));
}
}
}

View File

@@ -1234,11 +1234,10 @@ void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus
return;
adventureInt->onHeroChanged(hero);
if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING) && !gain)
{
//recalculate paths because hero has lost bonus influencing pathfinding
localState->erasePath(hero);
}
//recalculate paths because hero has lost or gained bonus influencing pathfinding
if (bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING || bonus.type == BonusType::ROUGH_TERRAIN_DISCOUNT || bonus.type == BonusType::NO_TERRAIN_PENALTY)
localState->verifyPath(hero);
}
void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path )
@@ -1602,9 +1601,6 @@ void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, SpellID sp
if(ENGINE->windows().topWindow<CSpellWindow>())
ENGINE->windows().popWindows(1);
if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK)
localState->erasePath(caster);
auto castSoundPath = spellID.toSpell()->getCastSound();
if(!castSoundPath.empty())
ENGINE->sound().playSound(castSoundPath);

View File

@@ -91,6 +91,16 @@ void PlayerLocalState::verifyPath(const CGHeroInstance * h)
setPath(h, getPath(h).endPos());
}
SpellID PlayerLocalState::getCurrentSpell() const
{
return currentSpell;
}
void PlayerLocalState::setCurrentSpell(SpellID castedSpell)
{
currentSpell = castedSpell;
}
const CGHeroInstance * PlayerLocalState::getCurrentHero() const
{
if(currentSelection && currentSelection->ID == Obj::HERO)

View File

@@ -49,6 +49,8 @@ class PlayerLocalState
PlayerSpellbookSetting spellbookSettings;
SpellID currentSpell;
void syncronizeState();
public:
@@ -89,6 +91,11 @@ public:
const CGTownInstance * getCurrentTown() const;
const CArmedInstance * getCurrentArmy() const;
// returns currently cast spell, if any
SpellID getCurrentSpell() const;
void setCurrentSpell(SpellID castedSpell);
void serialize(JsonNode & dest) const;
void deserialize(const JsonNode & source);

View File

@@ -47,6 +47,7 @@
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/pathfinder/CGPathNode.h"
#include "../../lib/pathfinder/TurnInfo.h"
#include "../../lib/spells/adventure/AdventureSpellEffect.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/spells/Problem.h"
@@ -515,7 +516,6 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
if(spellBeingCasted)
{
assert(shortcuts->optionSpellcasting());
assert(spellBeingCasted->id == SpellID::SCUTTLE_BOAT || spellBeingCasted->id == SpellID::DIMENSION_DOOR);
if(isValidAdventureSpellTarget(targetPosition))
performSpellcasting(targetPosition);
@@ -613,31 +613,16 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
if(spellBeingCasted)
{
switch(spellBeingCasted->id.toEnum())
{
case SpellID::SCUTTLE_BOAT:
if(isValidAdventureSpellTarget(targetPosition))
ENGINE->cursor().set(Cursor::Map::SCUTTLE_BOAT);
else
ENGINE->cursor().set(Cursor::Map::POINTER);
return;
const auto * hero = GAME->interface()->localState->getCurrentHero();
const auto * spellEffect = spellBeingCasted->getAdventureMechanics().getEffectAs<AdventureSpellRangedEffect>(hero);
spells::detail::ProblemImpl problem;
case SpellID::DIMENSION_DOOR:
if(isValidAdventureSpellTarget(targetPosition))
{
if(GAME->interface()->cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && GAME->interface()->cb->isTileGuardedUnchecked(targetPosition))
ENGINE->cursor().set(Cursor::Map::T1_ATTACK);
else
ENGINE->cursor().set(Cursor::Map::TELEPORT);
return;
}
else
ENGINE->cursor().set(Cursor::Map::POINTER);
return;
default:
if(spellEffect && spellEffect->canBeCastAtImpl(problem, GAME->interface()->cb.get(), hero, targetPosition))
ENGINE->cursor().set(spellEffect->getCursorForTarget(GAME->interface()->cb.get(), hero, targetPosition));
else
ENGINE->cursor().set(Cursor::Map::POINTER);
return;
}
return;
}
if(!isTargetPositionVisible)
@@ -839,11 +824,8 @@ void AdventureMapInterface::onTileRightClicked(const int3 &mapPos)
void AdventureMapInterface::enterCastingMode(const CSpell * sp)
{
assert(sp->id == SpellID::SCUTTLE_BOAT || sp->id == SpellID::DIMENSION_DOOR);
spellBeingCasted = sp;
Settings config = settings.write["session"]["showSpellRange"];
config->Bool() = true;
GAME->interface()->localState->setCurrentSpell(sp->id);
setState(EAdventureState::CASTING_SPELL);
}
@@ -852,9 +834,7 @@ void AdventureMapInterface::exitCastingMode()
assert(spellBeingCasted);
spellBeingCasted = nullptr;
setState(EAdventureState::MAKING_TURN);
Settings config = settings.write["session"]["showSpellRange"];
config->Bool() = false;
GAME->interface()->localState->setCurrentSpell(SpellID::NONE);
}
void AdventureMapInterface::hotkeyAbortCastingMode()

View File

@@ -117,9 +117,6 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
cellUnitMovementHighlight = ENGINE->renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
cellUnitMaxMovementHighlight = ENGINE->renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
attackCursors = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"), EImageBlitMode::COLORKEY);
spellCursors = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"), EImageBlitMode::COLORKEY);
rangedFullDamageLimitImages = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"), EImageBlitMode::COLORKEY);
shootingRangeLimitImages = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"), EImageBlitMode::COLORKEY);
@@ -861,24 +858,7 @@ void BattleFieldController::show(Canvas & to)
renderBattlefield(to);
if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID)
{
auto combatCursorIndex = ENGINE->cursor().get<Cursor::Combat>();
if (combatCursorIndex)
{
auto combatImageIndex = static_cast<size_t>(*combatCursorIndex);
to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - ENGINE->cursor().getPivotOffsetCombat(combatImageIndex));
return;
}
auto spellCursorIndex = ENGINE->cursor().get<Cursor::Spellcast>();
if (spellCursorIndex)
{
auto spellImageIndex = static_cast<size_t>(*spellCursorIndex);
to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - ENGINE->cursor().getPivotOffsetSpellcast());
return;
}
}
to.draw(ENGINE->cursor().getCurrentImage(), hexPositionAbsolute(getHoveredHex()).center() - ENGINE->cursor().getPivotOffset());
}
bool BattleFieldController::receiveEvent(const Point & position, int eventType) const

View File

@@ -37,9 +37,6 @@ class BattleFieldController : public CIntObject
std::shared_ptr<CAnimation> rangedFullDamageLimitImages;
std::shared_ptr<CAnimation> shootingRangeLimitImages;
std::shared_ptr<CAnimation> attackCursors;
std::shared_ptr<CAnimation> spellCursors;
/// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack
std::unique_ptr<Canvas> backgroundWithHexes;

View File

@@ -364,6 +364,8 @@ void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID)
void BattleInterface::spellCast(const BattleSpellCast * sc)
{
waitForAnimations();
// Do not deactivate anything in tactics mode
// This is battlefield setup spells
if(!tacticsMode)

View File

@@ -850,7 +850,7 @@ void BattleWindow::showAll(Canvas & to)
CIntObject::showAll(to);
if (ENGINE->screenDimensions().x != 800 || ENGINE->screenDimensions().y !=600)
to.drawBorder(Rect(pos.x-1, pos.y, pos.w+2, pos.h+1), Colors::BRIGHT_YELLOW);
to.drawBorder(Rect(pos.x-1, pos.y - (queue && queue->embedded ? 1 : 0), pos.w+2, pos.h+1 + (queue && queue->embedded ? 1 : 0)), Colors::BRIGHT_YELLOW);
}
void BattleWindow::show(Canvas & to)

View File

@@ -21,6 +21,7 @@
#include "../render/IRenderHandler.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/json/JsonUtils.h"
std::unique_ptr<ICursor> CursorHandler::createCursor()
{
@@ -42,7 +43,6 @@ CursorHandler::CursorHandler()
, showing(false)
, pos(0,0)
, dndObject(nullptr)
, type(Cursor::Type::DEFAULT)
{
showType = dynamic_cast<CursorSoftware *>(cursor.get()) ? Cursor::ShowType::SOFTWARE : Cursor::ShowType::HARDWARE;
}
@@ -51,49 +51,152 @@ CursorHandler::~CursorHandler() = default;
void CursorHandler::init()
{
cursors =
JsonNode cursorConfig = JsonUtils::assembleFromFiles("config/cursors.json");
std::vector<AnimationPath> animations;
for (const auto & cursorEntry : cursorConfig.Struct())
{
ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR"), EImageBlitMode::COLORKEY),
ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"), EImageBlitMode::COLORKEY),
ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT"), EImageBlitMode::COLORKEY),
ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"), EImageBlitMode::COLORKEY)
};
CursorParameters parameters;
parameters.cursorID = cursorEntry.first;
parameters.image = ImagePath::fromJson(cursorEntry.second["image"]);
parameters.animation = AnimationPath::fromJson(cursorEntry.second["animation"]);
parameters.animationFrameIndex = cursorEntry.second["frame"].Integer();
parameters.isAnimated = cursorEntry.second["animated"].Bool();
parameters.pivot.x = cursorEntry.second["pivotX"].Integer();
parameters.pivot.y = cursorEntry.second["pivotY"].Integer();
cursors.push_back(parameters);
}
set(Cursor::Map::POINTER);
}
void CursorHandler::changeGraphic(Cursor::Type type, size_t index)
void CursorHandler::set(const std::string & index)
{
assert(dndObject == nullptr);
if (type == this->type && index == this->frame)
if (index == currentCursorID)
return;
this->type = type;
this->frame = index;
currentCursorID = index;
currentCursorIndex = 0;
frameTime = 0;
for (size_t i = 0; i < cursors.size(); ++i)
{
if (cursors[i].cursorID == index)
{
currentCursorIndex = i;
break;
}
}
const auto & currentCursor = cursors.at(currentCursorIndex);
if (currentCursor.image.empty())
{
if (!loadedAnimations.count(currentCursor.animation))
loadedAnimations[currentCursor.animation] = ENGINE->renderHandler().loadAnimation(currentCursor.animation, EImageBlitMode::COLORKEY);
if (currentCursor.isAnimated)
cursorImage = loadedAnimations[currentCursor.animation]->getImage(0);
else
cursorImage = loadedAnimations[currentCursor.animation]->getImage(currentCursor.animationFrameIndex);
}
else
{
if (!loadedImages.count(currentCursor.image))
loadedImages[currentCursor.image] = ENGINE->renderHandler().loadImage(currentCursor.image, EImageBlitMode::COLORKEY);
cursorImage = loadedImages[currentCursor.image];
}
cursor->setImage(getCurrentImage(), getPivotOffset());
}
void CursorHandler::set(Cursor::Default index)
{
changeGraphic(Cursor::Type::DEFAULT, static_cast<size_t>(index));
}
void CursorHandler::set(Cursor::Map index)
{
changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
constexpr std::array mapCursorNames =
{
"mapPointer",
"mapHourglass",
"mapHero",
"mapTown",
"mapTurn1Move",
"mapTurn1Attack",
"mapTurn1Sail",
"mapTurn1Disembark",
"mapTurn1Exchange",
"mapTurn1Visit",
"mapTurn2Move",
"mapTurn2Attack",
"mapTurn2Sail",
"mapTurn2Disembark",
"mapTurn2Exchange",
"mapTurn2Visit",
"mapTurn3Move",
"mapTurn3Attack",
"mapTurn3Sail",
"mapTurn3Disembark",
"mapTurn3Exchange",
"mapTurn3Visit",
"mapTurn4Move",
"mapTurn4Attack",
"mapTurn4Sail",
"mapTurn4Disembark",
"mapTurn4Exchange",
"mapTurn4Visit",
"mapTurn1SailVisit",
"mapTurn2SailVisit",
"mapTurn3SailVisit",
"mapTurn4SailVisit",
"mapScrollNorth",
"mapScrollNorthEast",
"mapScrollEast",
"mapScrollSouthEast",
"mapScrollSouth",
"mapScrollSouthWest",
"mapScrollWest",
"mapScrollNorthWest",
"UNUSED",
"mapDimensionDoor",
"mapScuttleBoat"
};
set(mapCursorNames.at(static_cast<int>(index)));
}
void CursorHandler::set(Cursor::Combat index)
{
changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
constexpr std::array combatCursorNames =
{
"combatBlocked",
"combatMove",
"combatFly",
"combatShoot",
"combatHero",
"combatQuery",
"combatPointer",
"combatHitNorthEast",
"combatHitEast",
"combatHitSouthEast",
"combatHitSouthWest",
"combatHitWest",
"combatHitNorthWest",
"combatHitNorth",
"combatHitSouth",
"combatShootPenalty",
"combatShootCatapult",
"combatHeal",
"combatSacrifice",
"combatTeleport"
};
set(combatCursorNames.at(static_cast<int>(index)));
}
void CursorHandler::set(Cursor::Spellcast index)
{
//Note: this is animated cursor, ignore specified frame and only change type
changeGraphic(Cursor::Type::SPELLBOOK, frame);
//Note: this is animated cursor, ignore requested frame and only change type
set("castSpell");
}
void CursorHandler::dragAndDropCursor(std::shared_ptr<IImage> image)
@@ -116,120 +219,12 @@ void CursorHandler::cursorMove(const int & x, const int & y)
cursor->setCursorPosition(pos);
}
Point CursorHandler::getPivotOffsetDefault(size_t index)
{
return {0, 0};
}
Point CursorHandler::getPivotOffsetMap(size_t index)
{
static const std::array<Point, 43> offsets = {{
{ 0, 0}, // POINTER = 0,
{ 0, 0}, // HOURGLASS = 1,
{ 12, 10}, // HERO = 2,
{ 12, 12}, // TOWN = 3,
{ 15, 13}, // T1_MOVE = 4,
{ 13, 13}, // T1_ATTACK = 5,
{ 16, 32}, // T1_SAIL = 6,
{ 13, 20}, // T1_DISEMBARK = 7,
{ 8, 9}, // T1_EXCHANGE = 8,
{ 14, 16}, // T1_VISIT = 9,
{ 15, 13}, // T2_MOVE = 10,
{ 13, 13}, // T2_ATTACK = 11,
{ 16, 32}, // T2_SAIL = 12,
{ 13, 20}, // T2_DISEMBARK = 13,
{ 8, 9}, // T2_EXCHANGE = 14,
{ 14, 16}, // T2_VISIT = 15,
{ 15, 13}, // T3_MOVE = 16,
{ 13, 13}, // T3_ATTACK = 17,
{ 16, 32}, // T3_SAIL = 18,
{ 13, 20}, // T3_DISEMBARK = 19,
{ 8, 9}, // T3_EXCHANGE = 20,
{ 14, 16}, // T3_VISIT = 21,
{ 15, 13}, // T4_MOVE = 22,
{ 13, 13}, // T4_ATTACK = 23,
{ 16, 32}, // T4_SAIL = 24,
{ 13, 20}, // T4_DISEMBARK = 25,
{ 8, 9}, // T4_EXCHANGE = 26,
{ 14, 16}, // T4_VISIT = 27,
{ 16, 32}, // T1_SAIL_VISIT = 28,
{ 16, 32}, // T2_SAIL_VISIT = 29,
{ 16, 32}, // T3_SAIL_VISIT = 30,
{ 16, 32}, // T4_SAIL_VISIT = 31,
{ 6, 1}, // SCROLL_NORTH = 32,
{ 16, 2}, // SCROLL_NORTHEAST = 33,
{ 21, 6}, // SCROLL_EAST = 34,
{ 16, 16}, // SCROLL_SOUTHEAST = 35,
{ 6, 21}, // SCROLL_SOUTH = 36,
{ 1, 16}, // SCROLL_SOUTHWEST = 37,
{ 1, 5}, // SCROLL_WEST = 38,
{ 2, 1}, // SCROLL_NORTHWEST = 39,
{ 0, 0}, // POINTER_COPY = 40,
{ 14, 16}, // TELEPORT = 41,
{ 20, 20}, // SCUTTLE_BOAT = 42
}};
assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor
assert(index < offsets.size());
return offsets[index] * ENGINE->screenHandler().getScalingFactor();
}
Point CursorHandler::getPivotOffsetCombat(size_t index)
{
static const std::array<Point, 20> offsets = {{
{ 12, 12 }, // BLOCKED = 0,
{ 10, 14 }, // MOVE = 1,
{ 14, 14 }, // FLY = 2,
{ 12, 12 }, // SHOOT = 3,
{ 12, 12 }, // HERO = 4,
{ 8, 12 }, // QUERY = 5,
{ 0, 0 }, // POINTER = 6,
{ 21, 0 }, // HIT_NORTHEAST = 7,
{ 31, 5 }, // HIT_EAST = 8,
{ 21, 21 }, // HIT_SOUTHEAST = 9,
{ 0, 21 }, // HIT_SOUTHWEST = 10,
{ 0, 5 }, // HIT_WEST = 11,
{ 0, 0 }, // HIT_NORTHWEST = 12,
{ 6, 0 }, // HIT_NORTH = 13,
{ 6, 31 }, // HIT_SOUTH = 14,
{ 14, 0 }, // SHOOT_PENALTY = 15,
{ 12, 12 }, // SHOOT_CATAPULT = 16,
{ 12, 12 }, // HEAL = 17,
{ 12, 12 }, // SACRIFICE = 18,
{ 14, 20 }, // TELEPORT = 19
}};
assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor
assert(index < offsets.size());
return offsets[index] * ENGINE->screenHandler().getScalingFactor();
}
Point CursorHandler::getPivotOffsetSpellcast()
{
return Point(18, 28) * ENGINE->screenHandler().getScalingFactor();
}
Point CursorHandler::getPivotOffset()
{
if (dndObject)
return dndObject->dimensions() / 2;
switch (type) {
case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame);
case Cursor::Type::COMBAT: return getPivotOffsetCombat(frame);
case Cursor::Type::DEFAULT: return getPivotOffsetDefault(frame);
case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast();
};
assert(0);
return {0, 0};
return cursors.at(currentCursorIndex).pivot;
}
std::shared_ptr<IImage> CursorHandler::getCurrentImage()
@@ -237,15 +232,17 @@ std::shared_ptr<IImage> CursorHandler::getCurrentImage()
if (dndObject)
return dndObject;
return cursors[static_cast<size_t>(type)]->getImage(frame);
return cursorImage;
}
void CursorHandler::updateSpellcastCursor()
void CursorHandler::updateAnimatedCursor()
{
static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame
frameTime += ENGINE->framerate().getElapsedMilliseconds() / 1000.f;
size_t newFrame = frame;
int32_t newFrame = currentFrame;
const auto & animationName = cursors.at(currentCursorIndex).animation;
const auto & animation = loadedAnimations.at(animationName);
while (frameTime >= frameDisplayDuration)
{
@@ -253,12 +250,12 @@ void CursorHandler::updateSpellcastCursor()
newFrame++;
}
auto & animation = cursors.at(static_cast<size_t>(type));
while (newFrame >= animation->size())
newFrame -= animation->size();
changeGraphic(Cursor::Type::SPELLBOOK, newFrame);
currentFrame = newFrame;
cursorImage = animation->getImage(currentFrame);
cursor->setImage(getCurrentImage(), getPivotOffset());
}
void CursorHandler::render()
@@ -271,8 +268,8 @@ void CursorHandler::update()
if(!showing)
return;
if (type == Cursor::Type::SPELLBOOK)
updateSpellcastCursor();
if (cursors.at(currentCursorIndex).isAnimated)
updateAnimatedCursor();
cursor->update();
}

View File

@@ -18,25 +18,18 @@ class CAnimation;
namespace Cursor
{
enum class Type {
enum class Type : int8_t {
ADVENTURE, // set of various cursors for adventure map
COMBAT, // set of various cursors for combat
DEFAULT, // default arrow and hourglass cursors
SPELLBOOK // animated cursor for spellcasting
};
enum class ShowType {
enum class ShowType : int8_t {
SOFTWARE,
HARDWARE
};
enum class Default {
POINTER = 0,
//ARROW_COPY = 1, // probably unused
HOURGLASS = 2,
};
enum class Combat {
enum class Combat : int8_t {
BLOCKED = 0,
MOVE = 1,
FLY = 2,
@@ -61,7 +54,7 @@ namespace Cursor
COUNT
};
enum class Map {
enum class Map : int8_t {
POINTER = 0,
HOURGLASS = 1,
HERO = 2,
@@ -109,7 +102,7 @@ namespace Cursor
COUNT
};
enum class Spellcast {
enum class Spellcast : int8_t {
SPELL = 0,
};
}
@@ -117,26 +110,33 @@ namespace Cursor
/// handles mouse cursor
class CursorHandler final
{
struct CursorParameters
{
std::string cursorID;
ImagePath image;
AnimationPath animation;
Point pivot;
int animationFrameIndex;
bool isAnimated;
};
std::vector<CursorParameters> cursors;
std::map<AnimationPath, std::shared_ptr<CAnimation>> loadedAnimations;
std::map<ImagePath, std::shared_ptr<IImage>> loadedImages;
std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
std::array<std::shared_ptr<CAnimation>, 4> cursors;
bool showing;
std::shared_ptr<IImage> cursorImage; //if set, overrides currentCursor
/// Current cursor
Cursor::Type type;
Cursor::ShowType showType;
size_t frame;
float frameTime;
std::string currentCursorID;
Point pos;
float frameTime;
int32_t currentCursorIndex;
int32_t currentFrame;
Cursor::ShowType showType;
bool showing;
void changeGraphic(Cursor::Type type, size_t index);
Point getPivotOffset();
void updateSpellcastCursor();
std::shared_ptr<IImage> getCurrentImage();
void updateAnimatedCursor();
std::unique_ptr<ICursor> cursor;
@@ -154,31 +154,13 @@ public:
void dragAndDropCursor(const AnimationPath & path, size_t index);
/// Changes cursor to specified index
void set(Cursor::Default index);
void set(Cursor::Map index);
void set(Cursor::Combat index);
void set(Cursor::Spellcast index);
void set(const std::string & index);
/// Returns current index of cursor
template<typename Index>
std::optional<Index> get()
{
bool typeValid = true;
typeValid &= (std::is_same<Index, Cursor::Default>::value )|| type != Cursor::Type::DEFAULT;
typeValid &= (std::is_same<Index, Cursor::Map>::value )|| type != Cursor::Type::ADVENTURE;
typeValid &= (std::is_same<Index, Cursor::Combat>::value )|| type != Cursor::Type::COMBAT;
typeValid &= (std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK;
if (typeValid)
return static_cast<Index>(frame);
return std::nullopt;
}
Point getPivotOffsetSpellcast();
Point getPivotOffsetDefault(size_t index);
Point getPivotOffsetMap(size_t index);
Point getPivotOffsetCombat(size_t index);
std::shared_ptr<IImage> getCurrentImage();
Point getPivotOffset();
void render();
void update();

View File

@@ -19,14 +19,16 @@
#include "../GameInstance.h"
#include "../../lib/Point.h"
#include "../../lib/battle/CPlayerBattleCallback.h"
#include "../../lib/battle/IBattleState.h"
#include "../../lib/callback/CCallback.h"
#include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/mapObjects/MiscObjects.h"
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/mapping/CMap.h"
#include "../../lib/pathfinder/CGPathNode.h"
#include "../../lib/battle/CPlayerBattleCallback.h"
#include "../../lib/battle/IBattleState.h"
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/spells/adventure/AdventureSpellEffect.h"
MapRendererBaseContext::MapRendererBaseContext(const MapRendererContextState & viewState)
: viewState(viewState)
@@ -367,15 +369,14 @@ bool MapRendererAdventureContext::showTextOverlay() const
bool MapRendererAdventureContext::showSpellRange(const int3 & position) const
{
if (!settingSpellRange)
return false;
auto hero = GAME->interface()->localState->getCurrentHero();
auto spell = GAME->interface()->localState->getCurrentSpell();
if (!hero)
if (!hero || !spell.hasValue())
return false;
return !isInScreenRange(hero->getSightCenter(), position);
const auto * spellEffect = spell.toSpell()->getAdventureMechanics().getEffectAs<AdventureSpellRangedEffect>(hero);
return !spellEffect->isTargetInRange(GAME->interface()->cb.get(), hero, position);
}
MapRendererAdventureTransitionContext::MapRendererAdventureTransitionContext(const MapRendererContextState & viewState)

View File

@@ -72,7 +72,6 @@ public:
bool settingShowGrid = false;
bool settingShowVisitable = false;
bool settingShowBlocked = false;
bool settingSpellRange= false;
bool settingTextOverlay = false;
bool settingsAdventureObjectAnimation = true;
bool settingsAdventureTerrainAnimation = true;

View File

@@ -233,7 +233,6 @@ void MapViewController::updateState()
adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool();
adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool();
adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool();
adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool();
adventureContext->settingTextOverlay = (ENGINE->isKeyboardAltDown() || ENGINE->input().getNumTouchFingers() == 2) && settings["general"]["enableOverlay"].Bool();
}
}

View File

@@ -137,7 +137,7 @@ RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const Anim
auto it = animationLayouts.find(actualPath);
if (it != animationLayouts.end())
if (it != animationLayouts.end() && (settings["video"]["useHdTextures"].Bool() || scalingFactor == 1))
return it->second;
AnimationLayoutMap result;
@@ -312,9 +312,9 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
std::shared_ptr<SDLImageShared> img = nullptr;
if(CResourceHandler::get()->existsResource(imagePathSprites))
if(CResourceHandler::get()->existsResource(imagePathSprites) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1))
img = std::make_shared<SDLImageShared>(imagePathSprites, optimizeImage);
else if(CResourceHandler::get()->existsResource(imagePathData))
else if(CResourceHandler::get()->existsResource(imagePathData) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1))
img = std::make_shared<SDLImageShared>(imagePathData, optimizeImage);
else if(CResourceHandler::get()->existsResource(imagePath))
img = std::make_shared<SDLImageShared>(imagePath, optimizeImage);

View File

@@ -42,6 +42,7 @@
#include "../../lib/callback/CCallback.h"
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/spells/adventure/AdventureSpellEffect.h"
#include "../../lib/spells/Problem.h"
#include "../../lib/spells/SpellSchoolHandler.h"
#include "../../lib/texts/CGeneralTextHandler.h"
@@ -752,12 +753,12 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
spells::detail::ProblemImpl problem;
if (mySpell->getAdventureMechanics().canBeCast(problem, GAME->interface()->cb.get(), owner->myHero))
{
if(mySpell->getTargetType() == spells::AimType::LOCATION)
const auto * rangeEffect = mySpell->getAdventureMechanics().getEffectAs<AdventureSpellRangedEffect>(owner->myHero);
if(rangeEffect != nullptr)
adventureInt->enterCastingMode(mySpell);
else if(mySpell->getTargetType() == spells::AimType::NO_TARGET)
owner->myInt->cb->castSpell(h, mySpell->id);
else
logGlobal->error("Invalid spell target type");
owner->myInt->cb->castSpell(h, mySpell->id);
}
else
{

73
config/cursors.json Normal file
View File

@@ -0,0 +1,73 @@
/// This file can be modified via mods by creating file config/cursors.json
/// File in mod only needs to contain new or modified entries
/// Format:
/// - "image" - path to image (e.g. png file) with image for this cursor. Overrides "animation" key.
/// - "animation" - path to animation (.def or .json) with image for this cursor. Also requires "frame" or "animated" key
/// - "frame" - index of frame inside animation file that should be used for this cursor
/// - "animated" - if set to true, cursor will be animated using entire animation file
/// - "pivotX" and "pivotY" - position inside image that should act as pointer tip. Mouse clicks would generate click event on pixel below this point
{
"mapPointer" : { "pivotX" : 0, "pivotY" : 0, "animation" : "CRADVNTR", "frame" : 0 },
"mapHourglass" : { "pivotX" : 0, "pivotY" : 0, "animation" : "CRADVNTR", "frame" : 1 },
"mapHero" : { "pivotX" : 12, "pivotY" : 10, "animation" : "CRADVNTR", "frame" : 2 },
"mapTown" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRADVNTR", "frame" : 3 },
"mapTurn1Move" : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 4 },
"mapTurn1Attack" : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 5 },
"mapTurn1Sail" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 6 },
"mapTurn1Disembark" : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 7 },
"mapTurn1Exchange" : { "pivotX" : 8, "pivotY" : 9, "animation" : "CRADVNTR", "frame" : 8 },
"mapTurn1Visit" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 9 },
"mapTurn2Move" : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 10 },
"mapTurn2Attack" : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 11 },
"mapTurn2Sail" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 12 },
"mapTurn2Disembark" : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 13 },
"mapTurn2Exchange" : { "pivotX" : 8, "pivotY" : 9, "animation" : "CRADVNTR", "frame" : 14 },
"mapTurn2Visit" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 15 },
"mapTurn3Move" : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 16 },
"mapTurn3Attack" : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 17 },
"mapTurn3Sail" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 18 },
"mapTurn3Disembark" : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 19 },
"mapTurn3Exchange" : { "pivotX" : 8, "pivotY" : 9, "animation" : "CRADVNTR", "frame" : 20 },
"mapTurn3Visit" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 21 },
"mapTurn4Move" : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 22 },
"mapTurn4Attack" : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 23 },
"mapTurn4Sail" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 24 },
"mapTurn4Disembark" : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 25 },
"mapTurn4Exchange" : { "pivotX" : 8, "pivotY" : 9, "animation" : "CRADVNTR", "frame" : 26 },
"mapTurn4Visit" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 27 },
"mapTurn1SailVisit" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 28 },
"mapTurn2SailVisit" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 29 },
"mapTurn3SailVisit" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 30 },
"mapTurn4SailVisit" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 31 },
"mapScrollNorth" : { "pivotX" : 6, "pivotY" : 1, "animation" : "CRADVNTR", "frame" : 32 },
"mapScrollNorthEast" : { "pivotX" : 16, "pivotY" : 2, "animation" : "CRADVNTR", "frame" : 33 },
"mapScrollEast" : { "pivotX" : 21, "pivotY" : 6, "animation" : "CRADVNTR", "frame" : 34 },
"mapScrollSouthEast" : { "pivotX" : 16, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 35 },
"mapScrollSouth" : { "pivotX" : 6, "pivotY" : 21, "animation" : "CRADVNTR", "frame" : 36 },
"mapScrollSouthWest" : { "pivotX" : 1, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 37 },
"mapScrollWest" : { "pivotX" : 1, "pivotY" : 5, "animation" : "CRADVNTR", "frame" : 38 },
"mapScrollNorthWest" : { "pivotX" : 2, "pivotY" : 1, "animation" : "CRADVNTR", "frame" : 39 },
"mapDimensionDoor" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 41 },
"mapScuttleBoat" : { "pivotX" : 20, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 42 },
"combatBlocked" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 0},
"combatMove" : { "pivotX" : 10, "pivotY" : 14, "animation" : "CRCOMBAT", "frame" : 1},
"combatFly" : { "pivotX" : 14, "pivotY" : 14, "animation" : "CRCOMBAT", "frame" : 2},
"combatShoot" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 3},
"combatHero" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 4},
"combatQuery" : { "pivotX" : 8, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 5},
"combatPointer" : { "pivotX" : 0, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 6},
"combatHitNorthEast" : { "pivotX" : 21, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 7},
"combatHitEast" : { "pivotX" : 31, "pivotY" : 5, "animation" : "CRCOMBAT", "frame" : 8},
"combatHitSouthEast" : { "pivotX" : 21, "pivotY" : 21, "animation" : "CRCOMBAT", "frame" : 9},
"combatHitSouthWest" : { "pivotX" : 0, "pivotY" : 21, "animation" : "CRCOMBAT", "frame" : 10},
"combatHitWest" : { "pivotX" : 0, "pivotY" : 5, "animation" : "CRCOMBAT", "frame" : 11},
"combatHitNorthWest" : { "pivotX" : 0, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 12},
"combatHitNorth" : { "pivotX" : 6, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 13},
"combatHitSouth" : { "pivotX" : 6, "pivotY" : 31, "animation" : "CRCOMBAT", "frame" : 14},
"combatShootPenalty" : { "pivotX" : 14, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 15},
"combatShootCatapult" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 16},
"combatHeal" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 17},
"combatSacrifice" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 18},
"combatTeleport" : { "pivotX" : 14, "pivotY" : 20, "animation" : "CRCOMBAT", "frame" : 19},
"castSpell" : { "pivotX" : 18, "pivotY" : 28, "animation" : "CRSPELL", "animated" : true }
}

View File

@@ -220,7 +220,8 @@
"fontUpscalingFilter",
"downscalingFilter",
"allowPortrait",
"asyncUpscaling"
"asyncUpscaling",
"useHdTextures"
],
"properties" : {
"resolution" : {
@@ -316,6 +317,10 @@
"asyncUpscaling" : {
"type" : "boolean",
"default" : true
},
"useHdTextures" : {
"type" : "boolean",
"default" : true
}
}
},

View File

@@ -50,6 +50,335 @@
}
}
},
"adventureEffect" : {
"type" : "object",
"required" : [ "type" ],
"properties" : {
"type" : {
"type" : "string",
"enum" : [ "generic", "dimensionDoor", "removeObject", "summonBoat", "townPortal", "viewWorld" ]
},
"castsPerDay" : { "type" : "number" },
"castsPerDayXL" : { "type" : "number" },
"bonuses" : { "additionalProperties" : { "$ref" : "bonusInstance.json" }},
},
"allOf" : [
{
"if" : { "properties" : { "type" : { "const" : "dimensionDoor" } } },
"then" : {
"properties" : {
"castsPerDay" : {},
"castsPerDayXL" : {},
"bonuses" : {},
"type" : {},
"rangeX" : { "type" : "number" },
"rangeY" : { "type" : "number" },
"ignoreFow" : { "type" : "boolean" },
"cursor" : { "type" : "string" },
"cursorGuarded" : { "type" : "string" },
"movementPointsRequired" : { "type" : "number" },
"movementPointsTaken" : { "type" : "number" },
"waterLandFailureTakesPoints" : { "type" : "boolean" },
"exposeFow" : { "type" : "boolean" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "removeObject" } } },
"then" : {
"properties" : {
"castsPerDay" : {},
"castsPerDayXL" : {},
"bonuses" : {},
"type" : {},
"rangeX" : { "type" : "number" },
"rangeY" : { "type" : "number" },
"ignoreFow" : { "type" : "boolean" },
"cursor" : { "type" : "string" },
"objects" : { "additionalProperties" : { "type" : "boolean" } }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "summonBoat" } } },
"then" : {
"properties" : {
"castsPerDay" : {},
"castsPerDayXL" : {},
"bonuses" : {},
"type" : {},
"useExistingBoat" : { "type" : "boolean" },
"createdBoat" : { "type" : "string" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "townPortal" } } },
"then" : {
"properties" : {
"castsPerDay" : {},
"castsPerDayXL" : {},
"bonuses" : {},
"type" : {},
"movementPointsRequired" : { "type" : "number" },
"movementPointsTaken" : { "type" : "number" },
"allowTownSelection" : { "type" : "boolean" },
"skipOccupiedTowns" : { "type" : "boolean" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "viewWorld" } } },
"then" : {
"properties" : {
"castsPerDay" : {},
"castsPerDayXL" : {},
"bonuses" : {},
"type" : {},
"showTerrain" : { "type" : "boolean" },
"objects" : { "additionalProperties" : { "type" : "boolean" } }
},
"additionalProperties" : false
}
}
]
},
"battleEffect" : {
"type" : "object",
"required" : [ "type" ],
"properties" : {
"type" : {
"type" : "string",
"enum" : [
"core:catapult",
"core:clone",
"core:damage",
"core:demonSummon",
"core:dispel",
"core:heal",
"core:moat",
"core:obstacle",
"core:removeObstacle",
"core:sacrifice",
"core:summon",
"core:teleport",
"core:timed"
]
},
"indirect" : { "type" : "boolean" },
"optional" : { "type" : "boolean" }
},
"allOf" : [
{
"if" : { "properties" : { "type" : { "const" : "core:catapult" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"targetsToAttack" : { "type" : "number" },
"chanceToHitKeep" : { "type" : "number" },
"chanceToHitGate" : { "type" : "number" },
"chanceToHitTower" : { "type" : "number" },
"chanceToHitWall" : { "type" : "number" },
"chanceToNormalHit" : { "type" : "number" },
"chanceToCrit" : { "type" : "number" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:clone" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"ignoreImmunity" : { "type" : "boolean" },
"chainLength" : { "type" : "number" },
"chainFactor" : { "type" : "number" },
"maxTier" : { "type" : "number" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:damage" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"ignoreImmunity" : { "type" : "boolean" },
"chainLength" : { "type" : "number" },
"chainFactor" : { "type" : "number" },
"killByPercentage" : { "type" : "boolean" },
"killByCount" : { "type" : "boolean" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:demonSummon" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"ignoreImmunity" : { "type" : "boolean" },
"chainLength" : { "type" : "number" },
"chainFactor" : { "type" : "number" },
"id" : { "type" : "string" },
"permanent" : { "type" : "boolean" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:dispel" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"ignoreImmunity" : { "type" : "boolean" },
"chainLength" : { "type" : "number" },
"chainFactor" : { "type" : "number" },
"dispelPositive" : { "type" : "boolean" },
"dispelNegative" : { "type" : "boolean" },
"dispelNeutral" : { "type" : "boolean" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "enum" : [ "core:heal", "core:sacrifice" ] } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"ignoreImmunity" : { "type" : "boolean" },
"chainLength" : { "type" : "number" },
"chainFactor" : { "type" : "number" },
"healLevel" : { "type" : "string" },
"healPower" : { "type" : "string" },
"minFullUnits" : { "type" : "number" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:moat" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"hidden" : { "type" : "boolean" },
"trap" : { "type" : "boolean" },
"removeOnTrigger" : { "type" : "boolean" },
"dispellable" : { "type" : "boolean" },
"moatDamage" : { "type" : "number" },
"moatHexes" : { },
"triggerAbility" : { "type" : "string" },
"defender" : { },
"bonus" : { "additionalProperties" : { "$ref" : "bonusInstance.json" } }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:obstacle" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"hidden" : { "type" : "boolean" },
"passable" : { "type" : "boolean" },
"trap" : { "type" : "boolean" },
"removeOnTrigger" : { "type" : "boolean" },
"hideNative" : { "type" : "boolean" },
"patchCount" : { "type" : "number" },
"turnsRemaining" : { "type" : "number" },
"triggerAbility" : { "type" : "string" },
"attacker" : { },
"defender" : { },
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:removeObstacle" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"removeAbsolute" : { "type" : "boolean" },
"removeUsual" : { "type" : "boolean" },
"removeAllSpells" : { "type" : "boolean" },
"removeSpells" : { }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:summon" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"id" : { "type" : "string" },
"permanent" : { "type" : "boolean" },
"exclusive" : { "type" : "boolean" },
"summonByHealth" : { "type" : "boolean" },
"summonSameUnit" : { "type" : "boolean" },
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:teleport" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"ignoreImmunity" : { "type" : "boolean" },
"chainLength" : { "type" : "number" },
"chainFactor" : { "type" : "number" },
"triggerObstacles" : { "type" : "boolean" },
"isWallPassable" : { "type" : "boolean" },
"isMoatPassable" : { "type" : "boolean" }
},
"additionalProperties" : false
}
},
{
"if" : { "properties" : { "type" : { "const" : "core:timed" } } },
"then" : {
"properties" : {
"type" : {},
"indirect" : {},
"optional" : {},
"ignoreImmunity" : { "type" : "boolean" },
"chainLength" : { "type" : "number" },
"chainFactor" : { "type" : "number" },
"cumulative" : { "type" : "boolean" },
"bonus" : { "additionalProperties" : { "$ref" : "bonusInstance.json" } }
},
"additionalProperties" : false
}
},
]
},
"flags" : {
"type" : "object",
"additionalProperties" : {
@@ -94,9 +423,12 @@
"battleEffects" : {
"type" : "object",
"additionalProperties" : {
"type" : "object"
"$ref" : "#/definitions/battleEffect"
}
},
"adventureEffect" : {
"$ref" : "#/definitions/adventureEffect"
},
"targetModifier" : {
"type" : "object",
"additionalProperties" : false,

View File

@@ -6,9 +6,24 @@
"sounds": {
"cast": "SUMMBOAT"
},
"levels" :{
"levels" : {
"base":{
"range" : "X"
"range" : "X",
"adventureEffect" : {
"type" : "summonBoat",
"castsPerDay" : 0,
"useExistingBoat" : true
}
},
"advanced":{
"adventureEffect" : {
"createdBoat" : "boatNecropolis"
}
},
"expert":{
"adventureEffect" : {
"createdBoat" : "boatNecropolis"
}
}
},
"flags" : {
@@ -25,7 +40,26 @@
},
"levels" : {
"base":{
"range" : "X"
"range" : "X",
"adventureEffect" : {
"type" : "removeObject",
"castsPerDay" : 0,
"cursor" : "mapScuttleBoat", // defined in config/cursors.json
"rangeX" : 9,
"rangeY" : 8,
"ignoreFow" : false,
"objects" : {
"boat" : true
}
}
},
"advanced":{
"adventureEffect" : {
}
},
"expert":{
"adventureEffect" : {
}
}
},
"flags" : {
@@ -43,51 +77,58 @@
"levels" : {
"base":{
"range" : "0",
"effects" : {
"visionsMonsters" : {
"type" : "VISIONS",
"subtype" : "visionsMonsters",
"duration" : "ONE_DAY",
"val" : 1,
"valueType" : "INDEPENDENT_MAX"
"adventureEffect" : {
"type" : "generic",
"castsPerDay" : 0,
"bonuses" : {
"visionsMonsters" : {
"type" : "VISIONS",
"subtype" : "visionsMonsters",
"duration" : "ONE_DAY",
"val" : 1,
"valueType" : "INDEPENDENT_MAX"
}
}
}
}
},
"advanced":{
"effects" : {
"visionsMonsters" : {
"val" : 2
},
"visionsHeroes" :{
"type" : "VISIONS",
"subtype" : "visionsHeroes",
"duration" : "ONE_DAY",
"val" : 2,
"valueType" : "INDEPENDENT_MAX"
"adventureEffect" : {
"bonuses" : {
"visionsMonsters" : {
"val" : 2
},
"visionsHeroes" :{
"type" : "VISIONS",
"subtype" : "visionsHeroes",
"duration" : "ONE_DAY",
"val" : 2,
"valueType" : "INDEPENDENT_MAX"
}
}
}
}
},
"expert":{
"effects" : {
"visionsMonsters" : {
"val" : 3
},
"visionsHeroes" :{
"type" : "VISIONS",
"subtype" : "visionsHeroes",
"duration" : "ONE_DAY",
"val" : 3,
"valueType" : "INDEPENDENT_MAX"
},
"visionsTowns" :{
"type" : "VISIONS",
"subtype" : "visionsTowns",
"duration" : "ONE_DAY",
"val" : 3,
"valueType" : "INDEPENDENT_MAX"
"adventureEffect" : {
"bonuses" : {
"visionsMonsters" : {
"val" : 3
},
"visionsHeroes" :{
"type" : "VISIONS",
"subtype" : "visionsHeroes",
"duration" : "ONE_DAY",
"val" : 3,
"valueType" : "INDEPENDENT_MAX"
},
"visionsTowns" :{
"type" : "VISIONS",
"subtype" : "visionsTowns",
"duration" : "ONE_DAY",
"val" : 3,
"valueType" : "INDEPENDENT_MAX"
}
}
}
}
}
},
"flags" : {
@@ -103,7 +144,33 @@
},
"levels" : {
"base":{
"range" : "X"
"range" : "X",
"adventureEffect" : {
"type" : "viewWorld",
"castsPerDay" : 0,
"objects" : {
"resource" : true
}
}
},
"advanced":{
"adventureEffect" : {
"objects" : {
"resource" : true,
"mine" : true,
"abandonedMine" : true
}
}
},
"expert":{
"adventureEffect" : {
"objects" : {
"resource" : true,
"mine" : true,
"abandonedMine" : true
},
"showTerrain" : true
}
}
},
"flags" : {
@@ -120,28 +187,36 @@
"levels" : {
"base":{
"range" : "0",
"effects" : {
"stealth" : {
"type" : "DISGUISED",
"duration" : "ONE_DAY",
"val" : 1,
"valueType" : "INDEPENDENT_MAX"
"adventureEffect" : {
"type" : "generic",
"castsPerDay" : 0,
"bonuses" : {
"stealth" : {
"type" : "DISGUISED",
"duration" : "ONE_DAY",
"val" : 1,
"valueType" : "INDEPENDENT_MAX"
}
}
}
}
},
"advanced":{
"effects" : {
"stealth" : {
"val" : 2
}
}
"adventureEffect" : {
"bonuses" : {
"stealth" : {
"val" : 2
}
}
}
},
"expert":{
"effects" : {
"stealth" : {
"val" : 3
"adventureEffect" : {
"bonuses" : {
"stealth" : {
"val" : 3
}
}
}
}
}
},
"flags" : {
@@ -157,7 +232,31 @@
},
"levels" : {
"base":{
"range" : "X"
"range" : "X",
"adventureEffect" : {
"type" : "viewWorld",
"castsPerDay" : 0,
"objects" : {
"artifact" : true
}
}
},
"advanced":{
"adventureEffect" : {
"objects" : {
"artifact" : true,
"hero" : true
}
}
},
"expert":{
"adventureEffect" : {
"objects" : {
"artifact" : true,
"hero" : true,
"town" : true
}
}
}
},
"flags" : {
@@ -174,28 +273,38 @@
"levels" : {
"base":{
"range" : "0",
"effects" : {
"fly" : {
"type" : "FLYING_MOVEMENT",
"duration" : "ONE_DAY",
"val" : 40,
"valueType" : "INDEPENDENT_MIN"
"adventureEffect" : {
"type" : "generic",
"castsPerDay" : 0,
"bonuses" : {
"fly" : {
"type" : "FLYING_MOVEMENT",
"duration" : "ONE_DAY",
"val" : 40,
"valueType" : "INDEPENDENT_MIN"
}
}
}
}
},
"advanced":{
"effects" : {
"fly" : {
"val" : 20
"adventureEffect" : {
"type" : "generic",
"bonuses" : {
"fly" : {
"val" : 20
}
}
}
}
},
"expert":{
"effects" : {
"fly" : {
"val" : 0
"adventureEffect" : {
"type" : "generic",
"bonuses" : {
"fly" : {
"val" : 0
}
}
}
}
}
},
"flags" : {
@@ -212,28 +321,38 @@
"levels" : {
"base":{
"range" : "0",
"effects" : {
"waterWalk" : {
"type" : "WATER_WALKING",
"duration" : "ONE_DAY",
"val" : 40,
"valueType" : "INDEPENDENT_MIN"
"adventureEffect" : {
"type" : "generic",
"castsPerDay" : 0,
"bonuses" : {
"waterWalk" : {
"type" : "WATER_WALKING",
"duration" : "ONE_DAY",
"val" : 40,
"valueType" : "INDEPENDENT_MIN"
}
}
}
}
},
"advanced":{
"effects" : {
"waterWalk" : {
"val" : 20
"adventureEffect" : {
"type" : "generic",
"bonuses" : {
"waterWalk" : {
"val" : 20
}
}
}
}
},
"expert":{
"effects" : {
"waterWalk" : {
"val" : 0
"adventureEffect" : {
"type" : "generic",
"bonuses" : {
"waterWalk" : {
"val" : 0
}
}
}
}
}
},
"flags" : {
@@ -250,7 +369,31 @@
},
"levels" : {
"base":{
"range" : "X"
"range" : "X",
"adventureEffect" : {
"type" : "dimensionDoor",
"movementPointsRequired" : 0,
"movementPointsTaken" : 300,
"waterLandFailureTakesPoints" : true,
"cursor" : "mapDimensionDoor", // defined in config/cursors.json
"cursorGuarded" : "mapTurn1Attack", // defined in config/cursors.json
"castsPerDay" : 2,
"rangeX" : 9,
"rangeY" : 8,
"ignoreFow" : true,
"exposeFow" : true
}
},
"advanced":{
"adventureEffect" : {
"castsPerDay" : 3
}
},
"expert":{
"adventureEffect" : {
"castsPerDay" : 4,
"movementPointsTaken" : 200
}
}
},
"flags" : {
@@ -266,7 +409,29 @@
},
"levels" : {
"base":{
"range" : "X"
"range" : "X",
"adventureEffect" : {
"type" : "townPortal",
"castsPerDay" : 2,
"allowTownSelection" : false,
"skipOccupiedTowns" : false,
"movementPointsRequired" : 300,
"movementPointsTaken" : 300
}
},
"advanced":{
"adventureEffect" : {
"allowTownSelection" : true,
"movementPointsRequired" : 200,
"movementPointsTaken" : 200
}
},
"expert":{
"adventureEffect" : {
"allowTownSelection" : true,
"movementPointsRequired" : 200,
"movementPointsTaken" : 200
}
}
},
"flags" : {

View File

@@ -241,8 +241,6 @@ TODO
"firstEffect": {[bonus format]},
"secondEffect": {[bonus format]}
//...
},
// DEPRECATED, please use "battleEffects" with timed effect and "cumulative" set to true instead
@@ -260,6 +258,11 @@ TODO
"mod:secondEffect": {[effect format]}
//...
}
/// See Configurable adventure map effects section below for detailed description
"adventureEffect" : {
[effect format]
}
}
```
@@ -672,3 +675,179 @@ Value of all bonuses can be affected by following bonuses:
- range 0: any single obstacle
- range X: all obstacles
## Configurable adventure map effects
Currently, VCMI does not allow completely new spell effects for adventure maps. However, it is possible to:
- modify the parameters of all H3 spells.
- create spells with similar effects to H3 spells
- create a spell that gives bonuses to the hero who cast the spell.
Unlike combat effects, adventure map spells can only have one special effect, such as the Dimension Door or Town Portal effect. The number of bonuses granted by an adventure map spell is unlimited.
The AI has a limited understanding of adventure map spells and may use the following spells:
- Spells that give `WATER_WALKING` or `FLYING_MOVEMENT` bonuses
- Spells with the Summon Boat effect, provided the spell can create new boats with a 100% success chance.
- Any spells with the Town Portal effect.
### Common format
All properties in this section can be used for all non-generic adventure map spell effects.
Parameters:
- `type` - the type of spell effect used for this spell, or `generic` if a custom mechanic is not used.
- `castsPerDay` - Optional. Defines how many times a hero can cast this spell per day; set to zero or omit for unlimited use.
- `castsPerDayXL` - Optional. An alternative cast-per-day limit that is only active on maps that are at least XL+U in size. If this value is not set or is set to zero, the game will use the value of the `castsPerDay` variable.
- `bonuses` - A list of bonuses that will be given to the hero when this spell is cast successfully. When used with effects that can fail (e.g. Summon Boat), the bonuses will only apply to a successful cast.
Example:
```json
"adventureEffect" : {
"type" : "generic",
"castsPerDay" : 0,
"castsPerDayXL" : 0,
"bonuses" : {
"fly" : {
"type" : "FLYING_MOVEMENT",
"duration" : "ONE_DAY",
"val" : 40,
"valueType" : "INDEPENDENT_MIN"
}
}
}
```
### Dimension Door
The effect instantly teleports the hero to the selected location.
Parameters:
- `movementPointsRequired` - The amount of movement points the hero must have to cast this spell.
- `movementPointsTaken` - The amount of movement points that will be taken if the spell is cast successfully. If the hero does not have enough movement points, they will be reduced to zero after casting.
- `waterLandFailureTakesPoints` - If set to true, mana and movement points will be spent on an attempt to teleport to an inaccessible location (e.g. teleporting to land while in a boat).
- `cursor` - Identifier of the cursor that will be shown when hovering over a valid destination tile. See `config/cursors.json` for more details.
- `cursorGuarded` - alternative cursor that appears if using the teleport spell on a target would result in combat. This is only used if the game rule 'dimensionDoorTriggersGuards' is active.
- `exposeFow` - If this is set to true, using this spell will reveal information behind fog of war, such as whether teleportation is possible or if the location is guarded.
- `ignoreFow` - If this is set to true, it is possible to use the spell to teleport into terra incognita.
- `rangeX` - maximum distance to teleport in the X dimension (left-right axis).
- `rangeY` - maximum distance to teleport in the Y dimension (top-bottom axis).
Example:
```json
"adventureEffect" : {
"type" : "dimensionDoor",
"movementPointsRequired" : 0,
"movementPointsTaken" : 300,
"waterLandFailureTakesPoints" : true,
"cursor" : "mapDimensionDoor",
"cursorGuarded" : "mapTurn1Attack",
"castsPerDay" : 2,
"rangeX" : 9,
"rangeY" : 8,
"ignoreFow" : true,
"exposeFow" : true
}
```
### Remove Object
The effect completely removes the targeted object from the map. The Scuttle Boat spell is an example of this effect. The success chance is defined as [spell effect power](#spell-power).
Parameters:
- `objects` - a list of map objects that can be removed by this spell.
- `cursor` - identifier of the cursor that will be displayed when hovering over a valid target object. See `config/cursors.json` for more details.
- `ignoreFow` - If set to true, it is possible to use this spell to remove objects behind terra incognita.
- `rangeX` - maximum distance to remove objects in the X dimension (left-right axis).
- `rangeY` - maximum distance to remove objects in the Y dimension (top-bottom axis).
Example:
```json
"adventureEffect" : {
"type" : "removeObject",
"castsPerDay" : 0,
"cursor" : "mapScuttleBoat",
"rangeX" : 9,
"rangeY" : 8,
"ignoreFow" : false,
"objects" : {
"boat" : true
}
}
```
### Summon Boat
The effect moves or creates a boat next to the hero who cast the spell. The success chance is defined as [spell effect power](#spell-power).
Parameters:
- `useExistingBoat` - If this is set to true, the spell can move existing boats to the hero's location.
- `createdBoat` - Optional identifier of the boat type that can be created by this spell. If this is not set, the spell cannot create new boats.
Note that if the spell can both create new boats and use existing ones, it would prefer to move existing boats and only create new ones if there are no suitable ones to move.
Example:
```json
"adventureEffect" : {
"type" : "summonBoat",
"castsPerDay" : 0,
"useExistingBoat" : true,
"createdBoat" : "boatNecropolis"
}
```
### Town Portal
Effect moves hero to a location of owned or allied town.
Parameters:
- `movementPointsRequired` - amount of movement points that hero must have to cast this spell
- `movementPointsTaken` - amount of movement points that will be taken on sucessful cast of the spell. If hero does not have enough movement points, they will be reduced to zero after cast
- `allowTownSelection` - if set to true, player will be able to select town to teleport to among all friendly non-occupied towns.
- `skipOccupiedTowns` - if set to true, hero will teleport to nearest non-occupied town, ignoring any closer towns that are occupied by a visiting hero. No effect if `allowTownSelection` is set.
Example:
```json
"adventureEffect" : {
"type" : "townPortal",
"castsPerDay" : 2,
"allowTownSelection" : false,
"skipOccupiedTowns" : false,
"movementPointsRequired" : 300,
"movementPointsTaken" : 300
}
```
### View World
Effect shows World View menu with specified objects behind FoW revealed to the player
Parameters:
- `objects` - list of object types that will be revealed on World View. Note that only following objects have assotiated icon, any objects not from this list will not be visible: `resource`, `mine`, `abandonedMine`, `artifact`, `hero`, `town`.
- `showTerrain` - if set to true, terrain of the entire map (but not objects on it) will be revealed to the player.
Example:
```json
"adventureEffect" : {
"type" : "viewWorld",
"objects" : {
"resource" : true,
"mine" : true,
"abandonedMine" : true
},
"showTerrain" : true
}
```

View File

@@ -268,12 +268,13 @@ set(lib_MAIN_SRCS
spells/TargetCondition.cpp
spells/ViewSpellInt.cpp
spells/adventure/AdventureSpellEffect.cpp
spells/adventure/AdventureSpellMechanics.cpp
spells/adventure/DimensionDoorMechanics.cpp
spells/adventure/ScuttleBoatMechanics.cpp
spells/adventure/SummonBoatMechanics.cpp
spells/adventure/TownPortalMechanics.cpp
spells/adventure/ViewWorldMechanics.cpp
spells/adventure/DimensionDoorEffect.cpp
spells/adventure/RemoveObjectEffect.cpp
spells/adventure/SummonBoatEffect.cpp
spells/adventure/TownPortalEffect.cpp
spells/adventure/ViewWorldEffect.cpp
spells/effects/Catapult.cpp
spells/effects/Clone.cpp
@@ -731,11 +732,12 @@ set(lib_MAIN_HEADERS
spells/ViewSpellInt.h
spells/adventure/AdventureSpellMechanics.h
spells/adventure/DimensionDoorMechanics.h
spells/adventure/ScuttleBoatMechanics.h
spells/adventure/SummonBoatMechanics.h
spells/adventure/TownPortalMechanics.h
spells/adventure/ViewWorldMechanics.h
spells/adventure/AdventureSpellEffect.h
spells/adventure/DimensionDoorEffect.h
spells/adventure/RemoveObjectEffect.h
spells/adventure/SummonBoatEffect.h
spells/adventure/TownPortalEffect.h
spells/adventure/ViewWorldEffect.h
spells/effects/Catapult.h
spells/effects/Clone.h

View File

@@ -68,10 +68,6 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
{EGameSettings::CREATURES_JOINING_PERCENTAGE, "creatures", "joiningPercentage" },
{EGameSettings::CREATURES_WEEKLY_GROWTH_CAP, "creatures", "weeklyGrowthCap" },
{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" },
{EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" },
{EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" },
{EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles" },
{EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit" },
{EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" },
{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" },
{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" },

View File

@@ -41,10 +41,6 @@ enum class EGameSettings
CREATURES_JOINING_PERCENTAGE,
CREATURES_WEEKLY_GROWTH_CAP,
CREATURES_WEEKLY_GROWTH_PERCENT,
DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE,
DIMENSION_DOOR_FAILURE_SPENDS_POINTS,
DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES,
DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,
DIMENSION_DOOR_TRIGGERS_GUARDS,
DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,
DWELLINGS_ACCUMULATE_WHEN_OWNED,

View File

@@ -67,6 +67,10 @@ VCMI_LIB_NAMESPACE_BEGIN
getName = reinterpret_cast<TGetNameFun>(dlsym(dll, "GetAiName"));
getAI = reinterpret_cast<TGetAIFun>(dlsym(dll, methodName.c_str()));
}
else
{
logGlobal->error("Cannot open dynamic library '%s'. Reason: %s", libpath.string(), dlerror());
}
#endif // VCMI_WINDOWS
if (!dll)

View File

@@ -72,7 +72,7 @@ public:
//map
int3 guardingCreaturePosition (int3 pos) const override;
std::vector<const CGObjectInstance*> getGuardingCreatures (int3 pos) const override;
bool isTileGuardedUnchecked(int3 tile) const;
bool isTileGuardedUnchecked(int3 tile) const override;
const TerrainTile * getTile(int3 tile, bool verbose = true) const override;
const TerrainTile * getTileUnchecked(int3 tile) const override;
void getVisibleTilesInRange(std::unordered_set<int3> &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const;

View File

@@ -62,6 +62,11 @@ const TerrainTile * EditorCallback::getTileUnchecked(int3) const
THROW_EDITOR_UNSUPPORTED;
}
bool EditorCallback::isTileGuardedUnchecked(int3 tile) const
{
THROW_EDITOR_UNSUPPORTED;
}
const CGObjectInstance * EditorCallback::getTopObj(int3) const
{
THROW_EDITOR_UNSUPPORTED;

View File

@@ -32,6 +32,7 @@ public:
const TerrainTile * getTile(int3 tile, bool verbose) const override;
const TerrainTile * getTileUnchecked(int3 tile) const override;
bool isTileGuardedUnchecked(int3 tile) const override;
const CGObjectInstance * getTopObj(int3 pos) const override;
EDiggingStatus getTileDigStatus(int3 tile, bool verbose) const override;
void calculatePaths(const std::shared_ptr<PathfinderConfig> & config) const override;

View File

@@ -147,6 +147,8 @@ public:
virtual bool checkForVisitableDir(const int3 & src, const int3 & dst) const = 0;
/// Returns all wandering monsters that guard specified tile
virtual std::vector<const CGObjectInstance *> getGuardingCreatures (int3 pos) const = 0;
/// Returns if tile is guarded by wandering monsters without checking whether player has access to the tile. AVOID USAGE.
virtual bool isTileGuardedUnchecked(int3 tile) const = 0;
/// Returns all tiles within specified range with specific tile visibility mode
virtual void getTilesInRange(std::unordered_set<int3> & tiles, const int3 & pos, int radius, ETileVisibility mode, std::optional<PlayerColor> player = std::optional<PlayerColor>(), int3::EDistanceFormula formula = int3::DIST_2D) const = 0;

View File

@@ -856,16 +856,16 @@ public:
NONE = -1,
// Adventure map spells
SUMMON_BOAT = 0,
SCUTTLE_BOAT = 1,
VISIONS = 2,
VIEW_EARTH = 3,
DISGUISE = 4,
VIEW_AIR = 5,
FLY = 6,
WATER_WALK = 7,
DIMENSION_DOOR = 8,
TOWN_PORTAL = 9,
SUMMON_BOAT [[deprecated("check for spell mechanics instead of spell ID")]] = 0,
SCUTTLE_BOAT [[deprecated("check for spell mechanics instead of spell ID")]] = 1,
VISIONS [[deprecated("check for spell mechanics instead of spell ID")]] = 2,
VIEW_EARTH [[deprecated("check for spell mechanics instead of spell ID")]] = 3,
DISGUISE [[deprecated("check for spell mechanics instead of spell ID")]] = 4,
VIEW_AIR [[deprecated("check for spell mechanics instead of spell ID")]] = 5,
FLY [[deprecated("check for spell mechanics instead of spell ID")]] = 6,
WATER_WALK [[deprecated("check for spell mechanics instead of spell ID")]] = 7,
DIMENSION_DOOR [[deprecated("check for spell mechanics instead of spell ID")]] = 8,
TOWN_PORTAL [[deprecated("check for spell mechanics instead of spell ID")]] = 9,
// Combat spells
QUICKSAND = 10,

View File

@@ -25,6 +25,7 @@
#include "../mapObjects/MiscObjects.h"
#include "../mapping/CMap.h"
#include "../spells/CSpellHandler.h"
#include "spells/ISpellMechanics.h"
VCMI_LIB_NAMESPACE_BEGIN
@@ -502,7 +503,9 @@ CPathfinderHelper::CPathfinderHelper(const IGameInfoCallback & gameInfo, const C
turn(-1),
owner(Hero->tempOwner),
hero(Hero),
options(Options)
options(Options),
canCastFly(false),
canCastWaterWalk(false)
{
turnsInfo.reserve(16);
updateTurnInfo();
@@ -510,11 +513,20 @@ CPathfinderHelper::CPathfinderHelper(const IGameInfoCallback & gameInfo, const C
whirlpoolProtection = Hero->hasBonusOfType(BonusType::WHIRLPOOL_PROTECTION);
SpellID flySpell = SpellID::FLY;
canCastFly = Hero->canCastThisSpell(flySpell.toSpell());
if (options.canUseCast)
{
for (const auto & spell : LIBRARY->spellh->objects)
{
if (!spell || !spell->isAdventure())
continue;
SpellID waterWalk = SpellID::WATER_WALK;
canCastWaterWalk = Hero->canCastThisSpell(waterWalk.toSpell());
if(spell->getAdventureMechanics().givesBonus(hero, BonusType::WATER_WALKING) && hero->canCastThisSpell(spell.get()) && hero->mana >= hero->getSpellCost(spell.get()))
canCastWaterWalk = true;
if(spell->getAdventureMechanics().givesBonus(hero, BonusType::FLYING_MOVEMENT) && hero->canCastThisSpell(spell.get()) && hero->mana >= hero->getSpellCost(spell.get()))
canCastFly = true;
}
}
}
CPathfinderHelper::~CPathfinderHelper() = default;

View File

@@ -536,12 +536,6 @@ CSpell::TargetInfo::TargetInfo(const CSpell * spell, const int level, spells::Mo
clearAffected = levelInfo.clearAffected;
}
bool DLL_LINKAGE isInScreenRange(const int3 & center, const int3 & pos)
{
int3 diff = pos - center;
return diff.x >= -9 && diff.x <= 9 && diff.y >= -8 && diff.y <= 8;
}
///CSpellHandler
std::vector<JsonNode> CSpellHandler::loadLegacyData()
{
@@ -997,6 +991,8 @@ std::shared_ptr<CSpell> CSpellHandler::loadFromJson(const std::string & scope, c
levelObject.cumulativeEffects.push_back(b);
}
levelObject.adventureEffect = levelNode["adventureEffect"];
if(!levelNode["battleEffects"].Struct().empty())
{
levelObject.battleEffects = levelNode["battleEffects"];

View File

@@ -101,6 +101,7 @@ public:
std::vector<std::shared_ptr<Bonus>> cumulativeEffects; //deprecated
JsonNode battleEffects;
JsonNode adventureEffect;
};
/** \brief Low level accessor. Don`t use it if absolutely necessary
@@ -289,8 +290,6 @@ private:
std::unique_ptr<IAdventureSpellMechanics> adventureMechanics;//(!) do not serialize
};
bool DLL_LINKAGE isInScreenRange(const int3 &center, const int3 &pos); //for spells like Dimension Door
class DLL_LINKAGE CSpellHandler: public CHandlerBase<SpellID, spells::Spell, CSpell, spells::Service>
{
std::vector<int> spellRangeInHexes(std::string rng) const;

View File

@@ -27,11 +27,6 @@
#include "Problem.h"
#include "adventure/AdventureSpellMechanics.h"
#include "adventure/DimensionDoorMechanics.h"
#include "adventure/ScuttleBoatMechanics.h"
#include "adventure/SummonBoatMechanics.h"
#include "adventure/TownPortalMechanics.h"
#include "adventure/ViewWorldMechanics.h"
#include "BattleSpellMechanics.h"
@@ -648,28 +643,10 @@ IAdventureSpellMechanics::IAdventureSpellMechanics(const CSpell * s)
std::unique_ptr<IAdventureSpellMechanics> IAdventureSpellMechanics::createMechanics(const CSpell * s)
{
switch(s->id.toEnum())
{
case SpellID::SUMMON_BOAT:
return std::make_unique<SummonBoatMechanics>(s);
case SpellID::SCUTTLE_BOAT:
return std::make_unique<ScuttleBoatMechanics>(s);
case SpellID::DIMENSION_DOOR:
return std::make_unique<DimensionDoorMechanics>(s);
case SpellID::FLY:
case SpellID::WATER_WALK:
case SpellID::VISIONS:
case SpellID::DISGUISE:
return std::make_unique<AdventureSpellMechanics>(s); //implemented using bonus system
case SpellID::TOWN_PORTAL:
return std::make_unique<TownPortalMechanics>(s);
case SpellID::VIEW_EARTH:
return std::make_unique<ViewEarthMechanics>(s);
case SpellID::VIEW_AIR:
return std::make_unique<ViewAirMechanics>(s);
default:
return s->isCombat() ? std::unique_ptr<IAdventureSpellMechanics>() : std::make_unique<AdventureSpellMechanics>(s);
}
if (s->isCombat())
return nullptr;
return std::make_unique<AdventureSpellMechanics>(s);
}
VCMI_LIB_NAMESPACE_END

View File

@@ -30,6 +30,7 @@ class JsonNode;
class CStack;
class CGObjectInstance;
class CGHeroInstance;
class IAdventureSpellEffect;
namespace spells
{
@@ -354,11 +355,20 @@ public:
virtual bool canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const = 0;
virtual bool canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const = 0;
virtual bool adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const = 0;
static std::unique_ptr<IAdventureSpellMechanics> createMechanics(const CSpell * s);
virtual bool givesBonus(const spells::Caster * caster, BonusType which) const = 0;
template<typename EffectType>
const EffectType * getEffectAs(const spells::Caster * caster) const
{
return dynamic_cast<const EffectType *>(getEffect(caster));
}
protected:
virtual const IAdventureSpellEffect * getEffect(const spells::Caster * caster) const = 0;
const CSpell * owner;
};

View File

@@ -0,0 +1,46 @@
/*
* AdventureSpellEffect.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 "AdventureSpellEffect.h"
#include "../../json/JsonNode.h"
#include "../../mapObjects/CGHeroInstance.h"
#include "../../callback/IGameInfoCallback.h"
VCMI_LIB_NAMESPACE_BEGIN
AdventureSpellRangedEffect::AdventureSpellRangedEffect(const JsonNode & config)
: rangeX(config["rangeX"].Integer())
, rangeY(config["rangeY"].Integer())
, ignoreFow(config["ignoreFow"].Bool())
{
}
bool AdventureSpellRangedEffect::isTargetInRange(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
if(!cb->isInTheMap(pos))
return false;
if(caster->getHeroCaster())
{
int3 center = caster->getHeroCaster()->getSightCenter();
int3 diff = pos - center;
return diff.x >= -rangeX && diff.x <= rangeX && diff.y >= -rangeY && diff.y <= rangeY;
}
if(!ignoreFow && !cb->isVisibleFor(pos, caster->getCasterOwner()))
return false;
return true;
}
VCMI_LIB_NAMESPACE_END

View File

@@ -0,0 +1,60 @@
/*
* AdventureSpellEffect.h, 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
*
*/
#pragma once
#include "../ISpellMechanics.h"
VCMI_LIB_NAMESPACE_BEGIN
enum class ESpellCastResult : int8_t
{
OK, // cast successful
CANCEL, // cast failed but it is not an error, no mana has been spent
PENDING,
ERROR // error occurred, for example invalid request from player
};
class AdventureSpellMechanics;
class IAdventureSpellEffect
{
public:
virtual ~IAdventureSpellEffect() = default;
virtual ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const {return ESpellCastResult::OK;};
virtual ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const AdventureSpellMechanics & mechanics) const {return ESpellCastResult::OK;};
virtual void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const {};
virtual bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const {return true;};
virtual bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const {return true;};
virtual std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const {return {};};
};
class AdventureSpellEffect final : public IAdventureSpellEffect
{
public:
AdventureSpellEffect() = default;
};
class DLL_LINKAGE AdventureSpellRangedEffect : public IAdventureSpellEffect
{
int rangeX;
int rangeY;
bool ignoreFow;
public:
AdventureSpellRangedEffect(const JsonNode & config);
bool isTargetInRange(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const;
std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override = 0; //must be implemented in derived classes
bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override = 0; //must be implemented in derived classes
};
VCMI_LIB_NAMESPACE_END

View File

@@ -11,17 +11,86 @@
#include "StdInc.h"
#include "AdventureSpellMechanics.h"
#include "AdventureSpellEffect.h"
#include "DimensionDoorEffect.h"
#include "RemoveObjectEffect.h"
#include "SummonBoatEffect.h"
#include "TownPortalEffect.h"
#include "ViewWorldEffect.h"
#include "../CSpellHandler.h"
#include "../Problem.h"
#include "../../json/JsonBonus.h"
#include "../../mapObjects/CGHeroInstance.h"
#include "../../networkPacks/PacksForClient.h"
#include "../../callback/IGameInfoCallback.h"
VCMI_LIB_NAMESPACE_BEGIN
std::unique_ptr<IAdventureSpellEffect> AdventureSpellMechanics::createAdventureEffect(const CSpell * s, const JsonNode & node)
{
const std::string & typeID = node["type"].String();
if(typeID == "generic")
return std::make_unique<AdventureSpellEffect>();
if(typeID == "dimensionDoor")
return std::make_unique<DimensionDoorEffect>(s, node);
if(typeID == "removeObject")
return std::make_unique<RemoveObjectEffect>(s, node);
if(typeID == "summonBoat")
return std::make_unique<SummonBoatEffect>(s, node);
if(typeID == "townPortal")
return std::make_unique<TownPortalEffect>(s, node);
if(typeID == "viewWorld")
return std::make_unique<ViewWorldEffect>(s, node);
return std::make_unique<AdventureSpellEffect>();
}
AdventureSpellMechanics::AdventureSpellMechanics(const CSpell * s)
: IAdventureSpellMechanics(s)
{
for(int level = 0; level < GameConstants::SPELL_SCHOOL_LEVELS; level++)
{
const JsonNode & config = s->getLevelInfo(level).adventureEffect;
levelOptions[level].effect = createAdventureEffect(s, config);
levelOptions[level].castsPerDay = config["castsPerDay"].Integer();
levelOptions[level].castsPerDayXL = config["castsPerDayXL"].Integer();
levelOptions[level].bonuses = s->getLevelInfo(level).effects;
for(const auto & elem : config["bonuses"].Struct())
{
auto b = JsonUtils::parseBonus(elem.second);
b->sid = BonusSourceID(s->id);
b->source = BonusSource::SPELL_EFFECT;
levelOptions[level].bonuses.push_back(b);
}
}
}
AdventureSpellMechanics::~AdventureSpellMechanics() = default;
const AdventureSpellMechanics::LevelOptions & AdventureSpellMechanics::getLevel(const spells::Caster * caster) const
{
int schoolLevel = caster->getSpellSchoolLevel(owner);
return levelOptions.at(schoolLevel);
}
const IAdventureSpellEffect * AdventureSpellMechanics::getEffect(const spells::Caster * caster) const
{
return getLevel(caster).effect.get();
}
bool AdventureSpellMechanics::givesBonus(const spells::Caster * caster, BonusType which) const
{
for (const auto & bonus : getLevel(caster).bonuses)
if (bonus->type == which)
return true;
return false;
}
bool AdventureSpellMechanics::canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const
@@ -44,24 +113,30 @@ bool AdventureSpellMechanics::canBeCast(spells::Problem & problem, const IGameIn
if(heroCaster->mana < cost)
return false;
std::stringstream cachingStr;
cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num;
int castsAlreadyPerformedThisTurn = caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), cachingStr.str())->size();
int3 mapSize = cb->getMapSize();
bool mapSizeIsAtLeastXL = mapSize.x * mapSize.y * mapSize.z >= GameConstants::TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD;
bool useAlternativeLimit = mapSizeIsAtLeastXL && getLevel(caster).castsPerDayXL != 0;
int castsLimit = useAlternativeLimit ? getLevel(caster).castsPerDayXL : getLevel(caster).castsPerDay;
if(castsLimit > 0 && castsLimit <= castsAlreadyPerformedThisTurn ) //limit casts per turn
{
MetaString message = MetaString::createFromTextID("core.genrltxt.338");
caster->getCasterName(message);
problem.add(std::move(message));
return false;
}
}
return canBeCastImpl(problem, cb, caster);
return getLevel(caster).effect->canBeCastImpl(problem, cb, caster);
}
bool AdventureSpellMechanics::canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
return canBeCast(problem, cb, caster) && canBeCastAtImpl(problem, cb, caster, pos);
}
bool AdventureSpellMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const
{
return true;
}
bool AdventureSpellMechanics::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
return true;
return canBeCast(problem, cb, caster) && getLevel(caster).effect->canBeCastAtImpl(problem, cb, caster, pos);
}
bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
@@ -71,7 +146,7 @@ bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const Ad
if(!canBeCastAt(problem, env->getCb(), parameters.caster, parameters.pos))
return false;
ESpellCastResult result = beginCast(env, parameters);
ESpellCastResult result = getLevel(parameters.caster).effect->beginCast(env, parameters, *this);
if(result == ESpellCastResult::OK)
performCast(env, parameters);
@@ -79,43 +154,21 @@ bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const Ad
return result != ESpellCastResult::ERROR;
}
ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
void AdventureSpellMechanics::giveBonuses(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
if(owner->hasEffects())
for(const auto & b : getLevel(parameters.caster).bonuses)
{
//todo: cumulative effects support
const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
std::vector<Bonus> bonuses;
owner->getEffects(bonuses, schoolLevel, false, parameters.caster->getEnchantPower(owner));
for(const Bonus & b : bonuses)
{
GiveBonus gb;
gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId());
gb.bonus = b;
env->apply(gb);
}
return ESpellCastResult::OK;
GiveBonus gb;
gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId());
gb.bonus = *b;
gb.bonus.duration = parameters.caster->getEnchantPower(owner);
env->apply(gb);
}
else
{
//There is no generic algorithm of adventure cast
env->complain("Unimplemented adventure spell");
return ESpellCastResult::ERROR;
}
}
ESpellCastResult AdventureSpellMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
return ESpellCastResult::OK;
}
void AdventureSpellMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
// no-op, only for implementation in derived classes
GiveBonus gb;
gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId());
gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(owner->id));
env->apply(gb);
}
void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
@@ -128,16 +181,13 @@ void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const Adve
asc.spellID = owner->id;
env->apply(asc);
ESpellCastResult result = applyAdventureEffects(env, parameters);
ESpellCastResult result = getLevel(parameters.caster).effect->applyAdventureEffects(env, parameters);
switch(result)
if (result == ESpellCastResult::OK)
{
case ESpellCastResult::OK:
parameters.caster->spendMana(env, cost);
endCast(env, parameters);
break;
default:
break;
giveBonuses(env, parameters);
parameters.caster->spendMana(env, cost);
getLevel(parameters.caster).effect->endCast(env, parameters);
}
}

View File

@@ -14,33 +14,36 @@
VCMI_LIB_NAMESPACE_BEGIN
enum class ESpellCastResult
{
OK, // cast successful
CANCEL, // cast failed but it is not an error, no mana has been spent
PENDING,
ERROR // error occurred, for example invalid request from player
};
class IAdventureSpellEffect;
class AdventureSpellMechanics : public IAdventureSpellMechanics
class AdventureSpellMechanics final : public IAdventureSpellMechanics, boost::noncopyable
{
struct LevelOptions
{
std::unique_ptr<IAdventureSpellEffect> effect;
std::vector<std::shared_ptr<Bonus>> bonuses;
int castsPerDay;
int castsPerDayXL;
};
std::array<LevelOptions, GameConstants::SPELL_SCHOOL_LEVELS> levelOptions;
const LevelOptions & getLevel(const spells::Caster * caster) const;
void giveBonuses(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
std::unique_ptr<IAdventureSpellEffect> createAdventureEffect(const CSpell * s, const JsonNode & node);
public:
AdventureSpellMechanics(const CSpell * s);
bool canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final;
bool canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
bool adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override final;
protected:
///actual adventure cast implementation
virtual ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
virtual ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
virtual void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
virtual bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const;
virtual bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const;
~AdventureSpellMechanics();
void performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
private:
bool canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final;
bool canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
bool adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
const IAdventureSpellEffect * getEffect(const spells::Caster * caster) const final;
bool givesBonus(const spells::Caster * caster, BonusType which) const final;
};
VCMI_LIB_NAMESPACE_END

View File

@@ -0,0 +1,141 @@
/*
* DimensionDoorEffect.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 "DimensionDoorEffect.h"
#include "../CSpellHandler.h"
#include "../../IGameSettings.h"
#include "../../callback/IGameInfoCallback.h"
#include "../../mapObjects/CGHeroInstance.h"
#include "../../mapping/TerrainTile.h"
#include "../../networkPacks/PacksForClient.h"
VCMI_LIB_NAMESPACE_BEGIN
DimensionDoorEffect::DimensionDoorEffect(const CSpell * s, const JsonNode & config)
: AdventureSpellRangedEffect(config)
, cursor(config["cursor"].String())
, cursorGuarded(config["cursorGuarded"].String())
, movementPointsRequired(config["movementPointsRequired"].Integer())
, movementPointsTaken(config["movementPointsTaken"].Integer())
, waterLandFailureTakesPoints(config["waterLandFailureTakesPoints"].Bool())
, exposeFow(config["exposeFow"].Bool())
{
}
std::string DimensionDoorEffect::getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
if(!cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS))
return cursor;
if (!exposeFow && !cb->isVisibleFor(pos, caster->getCasterOwner()))
return cursor;
if (!cb->isTileGuardedUnchecked(pos))
return cursor;
return cursorGuarded;
}
bool DimensionDoorEffect::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const
{
if(!caster->getHeroCaster())
return false;
if(caster->getHeroCaster()->movementPointsRemaining() <= movementPointsRequired)
{
problem.add(MetaString::createFromTextID("core.genrltxt.125"));
return false;
}
return true;
}
bool DimensionDoorEffect::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
if (!isTargetInRange(cb, caster, pos))
return false;
int3 casterPosition = caster->getHeroCaster()->getSightCenter();
const TerrainTile * dest = cb->getTileUnchecked(pos);
const TerrainTile * curr = cb->getTileUnchecked(casterPosition);
if(!dest)
return false;
if(!curr)
return false;
if(exposeFow)
{
if(!dest->isClear(curr))
return false;
}
else
{
if(dest->blocked())
return false;
}
return true;
}
ESpellCastResult DimensionDoorEffect::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter();
const TerrainTile * dest = env->getCb()->getTile(parameters.pos);
const TerrainTile * curr = env->getCb()->getTile(casterPosition);
if(!dest->isClear(curr))
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
// tile is either blocked or not possible to move (e.g. water <-> land)
if(waterLandFailureTakesPoints)
{
// SOD: DD to such "wrong" terrain results in mana and move points spending, but fails to move hero
iw.text = MetaString::createFromTextID("core.genrltxt.70"); // Dimension Door failed!
env->apply(iw);
// no return - resources will be spent
}
else
{
// HotA: game will show error message without taking mana or move points, even when DD into terra incognita
iw.text = MetaString::createFromTextID("vcmi.dimensionDoor.seaToLandError");
env->apply(iw);
return ESpellCastResult::CANCEL;
}
}
SetMovePoints smp;
smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId());
if(movementPointsTaken < static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()))
smp.val = parameters.caster->getHeroCaster()->movementPointsRemaining() - movementPointsTaken;
else
smp.val = 0;
env->apply(smp);
return ESpellCastResult::OK;
}
void DimensionDoorEffect::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter();
const TerrainTile * dest = env->getCb()->getTile(parameters.pos);
const TerrainTile * curr = env->getCb()->getTile(casterPosition);
if(dest->isClear(curr))
env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), EMovementMode::DIMENSION_DOOR);
}
VCMI_LIB_NAMESPACE_END

View File

@@ -0,0 +1,37 @@
/*
* DimensionDoorEffect.h, 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
*
*/
#pragma once
#include "AdventureSpellEffect.h"
VCMI_LIB_NAMESPACE_BEGIN
class DimensionDoorEffect final : public AdventureSpellRangedEffect
{
std::string cursor;
std::string cursorGuarded;
int movementPointsRequired;
int movementPointsTaken;
bool waterLandFailureTakesPoints;
bool exposeFow;
public:
DimensionDoorEffect(const CSpell * s, const JsonNode & config);
private:
bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final;
bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
};
VCMI_LIB_NAMESPACE_END

View File

@@ -1,166 +0,0 @@
/*
* DimensionDoorMechanics.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 "DimensionDoorMechanics.h"
#include "../CSpellHandler.h"
#include "../../IGameSettings.h"
#include "../../callback/IGameInfoCallback.h"
#include "../../mapObjects/CGHeroInstance.h"
#include "../../mapping/TerrainTile.h"
#include "../../networkPacks/PacksForClient.h"
VCMI_LIB_NAMESPACE_BEGIN
DimensionDoorMechanics::DimensionDoorMechanics(const CSpell * s)
: AdventureSpellMechanics(s)
{
}
bool DimensionDoorMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const
{
if(!caster->getHeroCaster())
return false;
if(caster->getHeroCaster()->movementPointsRemaining() <= 0) //unlike town portal non-zero MP is enough
{
problem.add(MetaString::createFromTextID("core.genrltxt.125"));
return false;
}
const auto schoolLevel = caster->getSpellSchoolLevel(owner);
std::stringstream cachingStr;
cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num;
int castsAlreadyPerformedThisTurn = caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), cachingStr.str())->size();
int castsLimit = owner->getLevelPower(schoolLevel);
bool isTournamentRulesLimitEnabled = cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT);
if(isTournamentRulesLimitEnabled)
{
int3 mapSize = cb->getMapSize();
bool meetsTournamentRulesTwoCastsRequirements = mapSize.x * mapSize.y * mapSize.z >= GameConstants::TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD && schoolLevel == MasteryLevel::EXPERT;
castsLimit = meetsTournamentRulesTwoCastsRequirements ? 2 : 1;
}
if(castsAlreadyPerformedThisTurn >= castsLimit) //limit casts per turn
{
MetaString message = MetaString::createFromTextID("core.genrltxt.338");
caster->getCasterName(message);
problem.add(std::move(message));
return false;
}
return true;
}
bool DimensionDoorMechanics::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
if(!cb->isInTheMap(pos))
return false;
if(cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES))
{
if(!cb->isVisibleFor(pos, caster->getCasterOwner()))
return false;
}
int3 casterPosition = caster->getHeroCaster()->getSightCenter();
const TerrainTile * dest = cb->getTileUnchecked(pos);
const TerrainTile * curr = cb->getTileUnchecked(casterPosition);
if(!dest)
return false;
if(!curr)
return false;
if(!isInScreenRange(casterPosition, pos))
return false;
if(cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE))
{
if(!dest->isClear(curr))
return false;
}
else
{
if(dest->blocked())
return false;
}
return true;
}
ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
const int baseCost = env->getCb()->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
const int movementCost = baseCost * ((schoolLevel >= 3) ? 2 : 3);
int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter();
const TerrainTile * dest = env->getCb()->getTile(parameters.pos);
const TerrainTile * curr = env->getCb()->getTile(casterPosition);
if(!dest->isClear(curr))
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
// tile is either blocked or not possible to move (e.g. water <-> land)
if(env->getCb()->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS))
{
// SOD: DD to such "wrong" terrain results in mana and move points spending, but fails to move hero
iw.text = MetaString::createFromTextID("core.genrltxt.70"); // Dimension Door failed!
env->apply(iw);
// no return - resources will be spent
}
else
{
// HotA: game will show error message without taking mana or move points, even when DD into terra incognita
iw.text = MetaString::createFromTextID("vcmi.dimensionDoor.seaToLandError");
env->apply(iw);
return ESpellCastResult::CANCEL;
}
}
GiveBonus gb;
gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId());
gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(owner->id));
env->apply(gb);
SetMovePoints smp;
smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId());
if(movementCost < static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()))
smp.val = parameters.caster->getHeroCaster()->movementPointsRemaining() - movementCost;
else
smp.val = 0;
env->apply(smp);
return ESpellCastResult::OK;
}
void DimensionDoorMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter();
const TerrainTile * dest = env->getCb()->getTile(parameters.pos);
const TerrainTile * curr = env->getCb()->getTile(casterPosition);
if(dest->isClear(curr))
env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), EMovementMode::DIMENSION_DOOR);
}
VCMI_LIB_NAMESPACE_END

View File

@@ -1,30 +0,0 @@
/*
* DimensionDoorMechanics.h, 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
*
*/
#pragma once
#include "AdventureSpellMechanics.h"
VCMI_LIB_NAMESPACE_BEGIN
class DimensionDoorMechanics final : public AdventureSpellMechanics
{
public:
DimensionDoorMechanics(const CSpell * s);
protected:
bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const override;
bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override;
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
};
VCMI_LIB_NAMESPACE_END

View File

@@ -0,0 +1,84 @@
/*
* RemoveObjectEffect.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 "RemoveObjectEffect.h"
#include "../CSpellHandler.h"
#include "../../callback/IGameInfoCallback.h"
#include "../../mapObjects/CGHeroInstance.h"
#include "../../mapping/CMap.h"
#include "../../networkPacks/PacksForClient.h"
#include "../../modding/IdentifierStorage.h"
VCMI_LIB_NAMESPACE_BEGIN
RemoveObjectEffect::RemoveObjectEffect(const CSpell * s, const JsonNode & config)
: AdventureSpellRangedEffect(config)
, owner(s)
, failMessage(MetaString::createFromTextID("core.genrltxt.337")) //%s tried to scuttle the boat, but failed
, cursor(config["cursor"].String())
{
for(const auto & objectNode : config["objects"].Struct())
{
if(objectNode.second.Bool())
{
LIBRARY->identifiers()->requestIdentifier(objectNode.second.getModScope(), "object", objectNode.first, [this](si32 index)
{
removedObjects.push_back(MapObjectID(index));
});
}
}
}
std::string RemoveObjectEffect::getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
return cursor;
}
bool RemoveObjectEffect::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
if (!isTargetInRange(cb, caster, pos))
return false;
const TerrainTile * t = cb->getTileUnchecked(pos);
if(!t || t->visitableObjects.empty())
return false;
const CGObjectInstance * topObject = cb->getObj(t->visitableObjects.back());
return vstd::contains(removedObjects, topObject->ID);
}
ESpellCastResult RemoveObjectEffect::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
//check if spell works at all
if(env->getRNG()->nextInt(0, 99) >= owner->getLevelPower(schoolLevel)) //power is % chance of success
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
iw.text = failMessage;
parameters.caster->getCasterName(iw.text);
env->apply(iw);
return ESpellCastResult::OK;
}
const TerrainTile & t = env->getMap()->getTile(parameters.pos);
RemoveObject ro;
ro.initiator = parameters.caster->getCasterOwner();
ro.objectID = t.visitableObjects.back();
env->apply(ro);
return ESpellCastResult::OK;
}
VCMI_LIB_NAMESPACE_END

View File

@@ -0,0 +1,33 @@
/*
* RemoveObjectEffect.h, 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
*
*/
#pragma once
#include "AdventureSpellEffect.h"
VCMI_LIB_NAMESPACE_BEGIN
class RemoveObjectEffect final : public AdventureSpellRangedEffect
{
const CSpell * owner;
std::vector<MapObjectID> removedObjects;
MetaString failMessage;
std::string cursor;
public:
RemoveObjectEffect(const CSpell * s, const JsonNode & config);
private:
bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final;
};
VCMI_LIB_NAMESPACE_END

View File

@@ -1,78 +0,0 @@
/*
* ScuttleBoatMechanics.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 "ScuttleBoatMechanics.h"
#include "../CSpellHandler.h"
#include "../../callback/IGameInfoCallback.h"
#include "../../mapObjects/CGHeroInstance.h"
#include "../../mapping/CMap.h"
#include "../../networkPacks/PacksForClient.h"
VCMI_LIB_NAMESPACE_BEGIN
ScuttleBoatMechanics::ScuttleBoatMechanics(const CSpell * s)
: AdventureSpellMechanics(s)
{
}
bool ScuttleBoatMechanics::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
if(!cb->isInTheMap(pos))
return false;
if(caster->getHeroCaster())
{
int3 casterPosition = caster->getHeroCaster()->getSightCenter();
if(!isInScreenRange(casterPosition, pos))
return false;
}
if(!cb->isVisibleFor(pos, caster->getCasterOwner()))
return false;
const TerrainTile * t = cb->getTile(pos);
if(!t || t->visitableObjects.empty())
return false;
const CGObjectInstance * topObject = cb->getObj(t->visitableObjects.back());
if(topObject->ID != Obj::BOAT)
return false;
return true;
}
ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
//check if spell works at all
if(env->getRNG()->nextInt(0, 99) >= owner->getLevelPower(schoolLevel)) //power is % chance of success
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
iw.text.appendLocalString(EMetaText::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed
parameters.caster->getCasterName(iw.text);
env->apply(iw);
return ESpellCastResult::OK;
}
const TerrainTile & t = env->getMap()->getTile(parameters.pos);
RemoveObject ro;
ro.initiator = parameters.caster->getCasterOwner();
ro.objectID = t.visitableObjects.back();
env->apply(ro);
return ESpellCastResult::OK;
}
VCMI_LIB_NAMESPACE_END

View File

@@ -1,28 +0,0 @@
/*
* ScuttleBoatMechanics.h, 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
*
*/
#pragma once
#include "AdventureSpellMechanics.h"
VCMI_LIB_NAMESPACE_BEGIN
class ScuttleBoatMechanics final : public AdventureSpellMechanics
{
public:
ScuttleBoatMechanics(const CSpell * s);
protected:
bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override;
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
};
VCMI_LIB_NAMESPACE_END

View File

@@ -1,5 +1,5 @@
/*
* SummonBoatMechanics.cpp, part of VCMI engine
* SummonBoatEffect.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
@@ -10,23 +10,44 @@
#include "StdInc.h"
#include "SummonBoatMechanics.h"
#include "SummonBoatEffect.h"
#include "../CSpellHandler.h"
#include "../../mapObjects/CGHeroInstance.h"
#include "../../mapObjects/MiscObjects.h"
#include "../../mapping/CMap.h"
#include "../../modding/IdentifierStorage.h"
#include "../../networkPacks/PacksForClient.h"
VCMI_LIB_NAMESPACE_BEGIN
SummonBoatMechanics::SummonBoatMechanics(const CSpell * s)
: AdventureSpellMechanics(s)
SummonBoatEffect::SummonBoatEffect(const CSpell * s, const JsonNode & config)
: owner(s)
, useExistingBoat(config["useExistingBoat"].Bool())
{
if (!config["createdBoat"].isNull())
{
LIBRARY->identifiers()->requestIdentifier("core:boat", config["createdBoat"], [=](int32_t boatTypeID)
{
createdBoat = BoatId(boatTypeID);
});
}
}
bool SummonBoatMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const
bool SummonBoatEffect::canCreateNewBoat() const
{
return createdBoat != BoatId::NONE;
}
int SummonBoatEffect::getSuccessChance(const spells::Caster * caster) const
{
const auto schoolLevel = caster->getSpellSchoolLevel(owner);
return owner->getLevelPower(schoolLevel);
}
bool SummonBoatEffect::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const
{
if(!caster->getHeroCaster())
return false;
@@ -52,12 +73,10 @@ bool SummonBoatMechanics::canBeCastImpl(spells::Problem & problem, const IGameIn
return true;
}
ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
ESpellCastResult SummonBoatEffect::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
//check if spell works at all
if(env->getRNG()->nextInt(0, 99) >= owner->getLevelPower(schoolLevel)) //power is % chance of success
if(env->getRNG()->nextInt(0, 99) >= getSuccessChance(parameters.caster)) //power is % chance of success
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
@@ -69,17 +88,21 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
//try to find unoccupied boat to summon
const CGBoat * nearest = nullptr;
double dist = 0;
for(const auto & b : env->getMap()->getObjects<CGBoat>())
{
if(b->getBoardedHero() || b->layer != EPathfindingLayer::SAIL)
continue; //we're looking for unoccupied boat
double nDist = b->visitablePos().dist2d(parameters.caster->getHeroCaster()->visitablePos());
if(!nearest || nDist < dist) //it's first boat or closer than previous
if (useExistingBoat)
{
double dist = 0;
for(const auto & b : env->getMap()->getObjects<CGBoat>())
{
nearest = b;
dist = nDist;
if(b->getBoardedHero() || b->layer != EPathfindingLayer::SAIL)
continue; //we're looking for unoccupied boat
double nDist = b->visitablePos().dist2d(parameters.caster->getHeroCaster()->visitablePos());
if(!nearest || nDist < dist) //it's first boat or closer than previous
{
nearest = b;
dist = nDist;
}
}
}
@@ -93,7 +116,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
cop.initiator = parameters.caster->getCasterOwner();
env->apply(cop);
}
else if(schoolLevel < 2) //none or basic level -> cannot create boat :(
else if(!canCreateNewBoat()) //none or basic level -> cannot create boat :(
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
@@ -103,7 +126,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
}
else //create boat
{
env->createBoat(summonPos, BoatId::NECROPOLIS, parameters.caster->getCasterOwner());
env->createBoat(summonPos, createdBoat, parameters.caster->getCasterOwner());
}
return ESpellCastResult::OK;
}

View File

@@ -0,0 +1,34 @@
/*
* SummonBoatEffect.h, 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
*
*/
#pragma once
#include "AdventureSpellEffect.h"
VCMI_LIB_NAMESPACE_BEGIN
class DLL_LINKAGE SummonBoatEffect final : public IAdventureSpellEffect
{
const CSpell * owner;
BoatId createdBoat = BoatId::NONE;
bool useExistingBoat;
public:
SummonBoatEffect(const CSpell * s, const JsonNode & config);
bool canCreateNewBoat() const;
int getSuccessChance(const spells::Caster * caster) const;
private:
bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final;
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final;
};
VCMI_LIB_NAMESPACE_END

View File

@@ -1,5 +1,5 @@
/*
* TownPortalMechanics.cpp, part of VCMI engine
* TownPortalEffect.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
@@ -9,7 +9,9 @@
*/
#include "StdInc.h"
#include "TownPortalMechanics.h"
#include "TownPortalEffect.h"
#include "AdventureSpellMechanics.h"
#include "../CSpellHandler.h"
@@ -23,15 +25,18 @@
VCMI_LIB_NAMESPACE_BEGIN
TownPortalMechanics::TownPortalMechanics(const CSpell * s):
AdventureSpellMechanics(s)
TownPortalEffect::TownPortalEffect(const CSpell * s, const JsonNode & config)
: owner(s)
, movementPointsRequired(config["movementPointsRequired"].Integer())
, movementPointsTaken(config["movementPointsTaken"].Integer())
, allowTownSelection(config["allowTownSelection"].Bool())
, skipOccupiedTowns(config["skipOccupiedTowns"].Bool())
{
}
ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
ESpellCastResult TownPortalEffect::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
const CGTownInstance * destination = nullptr;
const int moveCost = movementCost(env, parameters);
if(!parameters.caster->getHeroCaster())
{
@@ -39,7 +44,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
return ESpellCastResult::ERROR;
}
if(parameters.caster->getSpellSchoolLevel(owner) < 2)
if(!allowTownSelection)
{
std::vector<const CGTownInstance *> pool = getPossibleTowns(env, parameters);
destination = findNearestTown(env, parameters, pool);
@@ -47,7 +52,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
if(nullptr == destination)
return ESpellCastResult::ERROR;
if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost)
if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < movementPointsRequired)
return ESpellCastResult::ERROR;
if(destination->getVisitingHero())
@@ -98,7 +103,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
return ESpellCastResult::ERROR;
}
if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost)
if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < movementPointsRequired)
{
env->complain("This hero has not enough movement points!");
return ESpellCastResult::ERROR;
@@ -131,9 +136,8 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
return ESpellCastResult::OK;
}
void TownPortalMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
void TownPortalEffect::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
const int moveCost = movementCost(env, parameters);
const CGTownInstance * destination = nullptr;
if(parameters.caster->getSpellSchoolLevel(owner) < 2)
@@ -154,12 +158,15 @@ void TownPortalMechanics::endCast(SpellCastEnvironment * env, const AdventureSpe
{
SetMovePoints smp;
smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId());
smp.val = std::max<ui32>(0, parameters.caster->getHeroCaster()->movementPointsRemaining() - moveCost);
if(movementPointsTaken < static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()))
smp.val = parameters.caster->getHeroCaster()->movementPointsRemaining() - movementPointsTaken;
else
smp.val = 0;
env->apply(smp);
}
}
ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
ESpellCastResult TownPortalEffect::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const AdventureSpellMechanics & mechanics) const
{
std::vector<const CGTownInstance *> towns = getPossibleTowns(env, parameters);
@@ -178,9 +185,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
return ESpellCastResult::CANCEL;
}
const int moveCost = movementCost(env, parameters);
if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost)
if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < movementPointsTaken)
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
@@ -191,7 +196,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
if(!parameters.pos.isValid() && parameters.caster->getSpellSchoolLevel(owner) >= 2)
{
auto queryCallback = [this, env, parameters](std::optional<int32_t> reply) -> void
auto queryCallback = [&mechanics, env, parameters](std::optional<int32_t> reply) -> void
{
if(reply.has_value())
{
@@ -213,7 +218,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
AdventureSpellCastParameters p;
p.caster = parameters.caster;
p.pos = o->visitablePos();
performCast(env, p);
mechanics.performCast(env, p);
}
};
@@ -247,7 +252,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
return ESpellCastResult::OK;
}
const CGTownInstance * TownPortalMechanics::findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector <const CGTownInstance *> & pool) const
const CGTownInstance * TownPortalEffect::findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector <const CGTownInstance *> & pool) const
{
if(pool.empty())
return nullptr;
@@ -271,7 +276,7 @@ const CGTownInstance * TownPortalMechanics::findNearestTown(SpellCastEnvironment
return *nearest;
}
std::vector<const CGTownInstance *> TownPortalMechanics::getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
std::vector<const CGTownInstance *> TownPortalEffect::getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
std::vector<const CGTownInstance *> ret;
@@ -281,19 +286,11 @@ std::vector<const CGTownInstance *> TownPortalMechanics::getPossibleTowns(SpellC
{
for(auto currTown : env->getCb()->getPlayerState(color)->getTowns())
{
ret.push_back(currTown);
if (!skipOccupiedTowns || currTown->getVisitingHero() == nullptr)
ret.push_back(currTown);
}
}
return ret;
}
int32_t TownPortalMechanics::movementCost(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
if(parameters.caster != parameters.caster->getHeroCaster()) //if caster is not hero
return 0;
int baseMovementCost = env->getCb()->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
return baseMovementCost * ((parameters.caster->getSpellSchoolLevel(owner) >= 3) ? 2 : 3);
}
VCMI_LIB_NAMESPACE_END

View File

@@ -1,5 +1,5 @@
/*
* TownPortalMechanics.h, part of VCMI engine
* TownPortalEffect.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
@@ -10,25 +10,31 @@
#pragma once
#include "AdventureSpellMechanics.h"
#include "AdventureSpellEffect.h"
VCMI_LIB_NAMESPACE_BEGIN
class CGTownInstance;
class TownPortalMechanics final : public AdventureSpellMechanics
class DLL_LINKAGE TownPortalEffect final : public IAdventureSpellEffect
{
public:
TownPortalMechanics(const CSpell * s);
const CSpell * owner;
int movementPointsRequired;
int movementPointsTaken;
bool allowTownSelection;
bool skipOccupiedTowns;
protected:
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
public:
TownPortalEffect(const CSpell * s, const JsonNode & config);
int getMovementPointsRequired() const { return movementPointsRequired; }
bool townSelectionAllowed() const { return allowTownSelection; }
private:
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const AdventureSpellMechanics & mechanics) const override;
void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
const CGTownInstance * findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector<const CGTownInstance *> & pool) const;
int32_t movementCost(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
std::vector<const CGTownInstance *> getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
};

View File

@@ -0,0 +1,68 @@
/*
* ViewWorldEffect.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 "ViewWorldEffect.h"
#include "../CSpellHandler.h"
#include "../../CPlayerState.h"
#include "../../callback/IGameInfoCallback.h"
#include "../../mapObjects/CGHeroInstance.h"
#include "../../mapping/CMap.h"
#include "../../modding/IdentifierStorage.h"
#include "../../networkPacks/PacksForClient.h"
VCMI_LIB_NAMESPACE_BEGIN
ViewWorldEffect::ViewWorldEffect(const CSpell * s, const JsonNode & config)
{
showTerrain = config["showTerrain"].Bool();
for(const auto & objectNode : config["objects"].Struct())
{
if(objectNode.second.Bool())
{
LIBRARY->identifiers()->requestIdentifier(objectNode.second.getModScope(), "object", objectNode.first, [this](si32 index)
{
filteredObjects.push_back(MapObjectID(index));
});
}
}
}
ESpellCastResult ViewWorldEffect::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
ShowWorldViewEx pack;
pack.player = parameters.caster->getCasterOwner();
pack.showTerrain = showTerrain;
const auto & fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap;
for(const auto & obj : env->getMap()->getObjects())
{
//deleted object remain as empty pointer
if(obj && vstd::contains(filteredObjects, obj->ID))
{
ObjectPosInfo posInfo(obj);
if(fowMap[posInfo.pos.z][posInfo.pos.x][posInfo.pos.y] == 0)
pack.objectPositions.push_back(posInfo);
}
}
env->apply(pack);
return ESpellCastResult::OK;
}
VCMI_LIB_NAMESPACE_END

View File

@@ -1,5 +1,5 @@
/*
* SummonBoatMechanics.h, part of VCMI engine
* ViewWorldEffect.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
@@ -10,17 +10,17 @@
#pragma once
#include "AdventureSpellMechanics.h"
#include "AdventureSpellEffect.h"
VCMI_LIB_NAMESPACE_BEGIN
class SummonBoatMechanics final : public AdventureSpellMechanics
class ViewWorldEffect final : public IAdventureSpellEffect
{
public:
SummonBoatMechanics(const CSpell * s);
std::vector<MapObjectID> filteredObjects;
bool showTerrain = false;
protected:
bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const override;
public:
ViewWorldEffect(const CSpell * s, const JsonNode & config);
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
};

View File

@@ -1,90 +0,0 @@
/*
* ViewWorldMechanics.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 "ViewWorldMechanics.h"
#include "../CSpellHandler.h"
#include "../../CPlayerState.h"
#include "../../callback/IGameInfoCallback.h"
#include "../../mapObjects/CGHeroInstance.h"
#include "../../mapping/CMap.h"
#include "../../networkPacks/PacksForClient.h"
VCMI_LIB_NAMESPACE_BEGIN
ViewMechanics::ViewMechanics(const CSpell * s):
AdventureSpellMechanics(s)
{
}
ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
ShowWorldViewEx pack;
pack.player = parameters.caster->getCasterOwner();
const auto spellLevel = parameters.caster->getSpellSchoolLevel(owner);
const auto & fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap;
for(const auto & obj : env->getMap()->getObjects())
{
//deleted object remain as empty pointer
if(obj && filterObject(obj, spellLevel))
{
ObjectPosInfo posInfo(obj);
if(fowMap[posInfo.pos.z][posInfo.pos.x][posInfo.pos.y] == 0)
pack.objectPositions.push_back(posInfo);
}
}
pack.showTerrain = showTerrain(spellLevel);
env->apply(pack);
return ESpellCastResult::OK;
}
///ViewAirMechanics
ViewAirMechanics::ViewAirMechanics(const CSpell * s)
: ViewMechanics(s)
{
}
bool ViewAirMechanics::filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const
{
return (obj->ID == Obj::ARTIFACT) || (spellLevel > 1 && obj->ID == Obj::HERO) || (spellLevel > 2 && obj->ID == Obj::TOWN);
}
bool ViewAirMechanics::showTerrain(const int32_t spellLevel) const
{
return false;
}
///ViewEarthMechanics
ViewEarthMechanics::ViewEarthMechanics(const CSpell * s)
: ViewMechanics(s)
{
}
bool ViewEarthMechanics::filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const
{
return (obj->ID == Obj::RESOURCE) || (spellLevel > 1 && obj->ID == Obj::MINE);
}
bool ViewEarthMechanics::showTerrain(const int32_t spellLevel) const
{
return spellLevel > 2;
}
VCMI_LIB_NAMESPACE_END

View File

@@ -1,48 +0,0 @@
/*
* ViewWorldMechanics.h, 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
*
*/
#pragma once
#include "AdventureSpellMechanics.h"
VCMI_LIB_NAMESPACE_BEGIN
class ViewMechanics : public AdventureSpellMechanics
{
public:
ViewMechanics(const CSpell * s);
protected:
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
virtual bool filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const = 0;
virtual bool showTerrain(const int32_t spellLevel) const = 0;
};
class ViewAirMechanics final : public ViewMechanics
{
public:
ViewAirMechanics(const CSpell * s);
protected:
bool filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const override;
bool showTerrain(const int32_t spellLevel) const override;
};
class ViewEarthMechanics final : public ViewMechanics
{
public:
ViewEarthMechanics(const CSpell * s);
protected:
bool filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const override;
bool showTerrain(const int32_t spellLevel) const override;
};
VCMI_LIB_NAMESPACE_END

View File

@@ -78,6 +78,7 @@ public:
MOCK_CONST_METHOD1(guardingCreaturePosition, int3(int3 pos));
MOCK_CONST_METHOD2(checkForVisitableDir, bool(const int3 & src, const int3 & dst));
MOCK_CONST_METHOD1(getGuardingCreatures, std::vector<const CGObjectInstance *>(int3 pos));
MOCK_CONST_METHOD1(isTileGuardedUnchecked, bool(int3 pos));
MOCK_METHOD2(pickAllowedArtsSet, void(std::vector<ArtifactID> & out, vstd::RNG & rand));