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

Small refactoring of adventure map spell casting:

- Removed duplicated checks for DD possibility
- Moved most of spell-specific code from AdventureMapInterface to library
code
- AdventureSpellMechanics class now provides methods to check whether
spellcast is possible, similarly to battle spells
- If it is not possible to cast adventure map spell (e.g. no mana or no
move points) game will show infowindow immediately on clicking spellbook
instead of on cast attempt
- If hero does not have movement points for a DD, game will show correct
error message
- Added game settings 'dimensionDoorFailureSpendsPoints' due to
discovered H3 logic
This commit is contained in:
Ivan Savenko 2024-04-10 18:19:48 +03:00
parent bcd4a8c961
commit 8353bca34f
15 changed files with 285 additions and 289 deletions

View File

@ -118,6 +118,8 @@
"vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!",
"vcmi.dimensionDoor.seaToLandError" : "It's not possible to teleport from sea to land or vice versa with a Dimension Door.",
"vcmi.settingsMainWindow.generalTab.hover" : "General",
"vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.",
"vcmi.settingsMainWindow.battleTab.hover" : "Battle",

View File

@ -113,6 +113,8 @@
"vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!",
"vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами",
"vcmi.settingsMainWindow.generalTab.hover" : "Загальні",
"vcmi.settingsMainWindow.generalTab.help" : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта",
"vcmi.settingsMainWindow.battleTab.hover" : "Бої",

View File

@ -45,6 +45,8 @@
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/mapping/CMapDefines.h"
#include "../../lib/pathfinder/CGPathNode.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/spells/Problem.h"
std::shared_ptr<AdventureMapInterface> adventureInt;
@ -506,11 +508,6 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
if(!shortcuts->optionMapViewActive())
return;
if(!LOCPLINT->cb->isVisible(targetPosition))
{
if(!spellBeingCasted || spellBeingCasted->id != SpellID::DIMENSION_DOOR)
return;
}
if(!LOCPLINT->makingTurn)
return;
@ -519,23 +516,16 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
if(spellBeingCasted)
{
assert(shortcuts->optionSpellcasting());
assert(spellBeingCasted->id == SpellID::SCUTTLE_BOAT || spellBeingCasted->id == SpellID::DIMENSION_DOOR);
switch(spellBeingCasted->id)
{
case SpellID::SCUTTLE_BOAT:
if(isValidAdventureSpellTarget(targetPosition, topBlocking, SpellID::SCUTTLE_BOAT))
if(isValidAdventureSpellTarget(targetPosition))
performSpellcasting(targetPosition);
break;
case SpellID::DIMENSION_DOOR:
if(isValidAdventureSpellTarget(targetPosition, topBlocking, SpellID::DIMENSION_DOOR))
performSpellcasting(targetPosition);
break;
default:
break;
}
return;
}
if(!LOCPLINT->cb->isVisible(targetPosition))
return;
//check if we can select this object
bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID;
canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner) != PlayerRelations::ENEMIES;
@ -610,21 +600,45 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
return;
bool isTargetPositionVisible = LOCPLINT->cb->isVisible(targetPosition);
const CGObjectInstance *objAtTile = isTargetPositionVisible ? getActiveObject(targetPosition) : nullptr;
if(!isTargetPositionVisible)
if(spellBeingCasted)
{
GH.statusbar()->clear();
switch(spellBeingCasted->id)
{
case SpellID::SCUTTLE_BOAT:
if(isValidAdventureSpellTarget(targetPosition))
CCS->curh->set(Cursor::Map::SCUTTLE_BOAT);
else
CCS->curh->set(Cursor::Map::POINTER);
return;
if(!spellBeingCasted || spellBeingCasted->id != SpellID::DIMENSION_DOOR)
case SpellID::DIMENSION_DOOR:
if(isValidAdventureSpellTarget(targetPosition))
{
if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && LOCPLINT->cb->isTileGuardedUnchecked(targetPosition))
CCS->curh->set(Cursor::Map::T1_ATTACK);
else
CCS->curh->set(Cursor::Map::TELEPORT);
return;
}
else
CCS->curh->set(Cursor::Map::POINTER);
return;
default:
CCS->curh->set(Cursor::Map::POINTER);
return;
}
}
if(!isTargetPositionVisible)
{
CCS->curh->set(Cursor::Map::POINTER);
return;
}
auto objRelations = PlayerRelations::ALLIES;
const CGObjectInstance *objAtTile = isTargetPositionVisible ? getActiveObject(targetPosition) : nullptr;
if(objAtTile)
{
objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner);
@ -638,40 +652,6 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
GH.statusbar()->write(tileTooltipText);
}
if(spellBeingCasted)
{
switch(spellBeingCasted->id)
{
case SpellID::SCUTTLE_BOAT:
if(isValidAdventureSpellTarget(targetPosition, objAtTile, SpellID::SCUTTLE_BOAT))
CCS->curh->set(Cursor::Map::SCUTTLE_BOAT);
else
CCS->curh->set(Cursor::Map::POINTER);
return;
case SpellID::DIMENSION_DOOR:
if(isValidAdventureSpellTarget(targetPosition, objAtTile, SpellID::DIMENSION_DOOR))
{
if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS))
{
auto isGuarded = LOCPLINT->cb->isTileGuardedAfterDimensionDoorUse(targetPosition, LOCPLINT->localState->getCurrentHero());
if(isGuarded)
{
CCS->curh->set(Cursor::Map::T1_ATTACK);
return;
}
}
CCS->curh->set(Cursor::Map::TELEPORT);
}
else
CCS->curh->set(Cursor::Map::POINTER);
return;
default:
CCS->curh->set(Cursor::Map::POINTER);
return;
}
}
if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN || GH.isKeyboardCtrlDown())
{
if(objAtTile)
@ -945,34 +925,9 @@ void AdventureMapInterface::onScreenResize()
activate();
}
bool AdventureMapInterface::isValidAdventureSpellTarget(int3 targetPosition, const CGObjectInstance * topObjectAtTarget, SpellID spellId)
bool AdventureMapInterface::isValidAdventureSpellTarget(int3 targetPosition)
{
int3 heroPosition = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
if (!isInScreenRange(heroPosition, targetPosition))
{
return false;
}
spells::detail::ProblemImpl problem;
switch(spellId)
{
case SpellID::SCUTTLE_BOAT:
{
if(topObjectAtTarget && topObjectAtTarget->ID == Obj::BOAT)
return true;
else
return false;
}
case SpellID::DIMENSION_DOOR:
{
const TerrainTile * t = LOCPLINT->cb->getTileForDimensionDoor(targetPosition, LOCPLINT->localState->getCurrentHero());
if(t && t->isClear(LOCPLINT->cb->getTile(heroPosition)))
return true;
else
return false;
}
default:
logGlobal->warn("Called AdventureMapInterface::isValidAdventureSpellTarget with unknown Spell ID!");
return false;
}
return spellBeingCasted->getAdventureMechanics().canBeCastAt(problem, LOCPLINT->cb.get(), LOCPLINT->localState->getCurrentHero(), targetPosition);
}

View File

@ -93,7 +93,7 @@ private:
void performSpellcasting(const int3 & castTarget);
/// performs clientside validation of valid targets for adventure spells
bool isValidAdventureSpellTarget(int3 targetPosition, const CGObjectInstance * topObjectAtTarget, SpellID spellId);
bool isValidAdventureSpellTarget(int3 targetPosition);
/// dim interface if some windows opened
void dim(Canvas & to);

View File

@ -40,6 +40,7 @@
#include "../../lib/CConfigHandler.h"
#include "../../lib/CGeneralTextHandler.h"
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/spells/Problem.h"
#include "../../lib/GameConstants.h"
@ -653,6 +654,9 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
owner->myInt->localState->spellbookSettings.spellbookLastPageAdvmap = owner->currentPage;
});
spells::detail::ProblemImpl problem;
if (mySpell->getAdventureMechanics().canBeCast(problem, LOCPLINT->cb.get(), owner->myHero))
{
if(mySpell->getTargetType() == spells::AimType::LOCATION)
adventureInt->enterCastingMode(mySpell);
else if(mySpell->getTargetType() == spells::AimType::NO_TARGET)
@ -660,6 +664,16 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
else
logGlobal->error("Invalid spell target type");
}
else
{
std::vector<std::string> texts;
problem.getAll(texts);
if(!texts.empty())
LOCPLINT->showInfoDialog(texts.front());
else
LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem"));
}
}
}
}

View File

@ -395,8 +395,10 @@
"dimensionDoorOnlyToUncoveredTiles" : false,
// if enabled, dimension door will hint regarding tile being incompatible terrain type, unlike H3 (water/land)
"dimensionDoorExposesTerrainType" : false,
// if enabled, attempt to use dimension door on incompatible terrain (water/land) will result in spending of mana, movement and casts per day (H3 behavior)
"dimensionDoorFailureSpendsPoints" : true,
// if enabled, dimension door will initiate a fight upon landing on tile adjacent to neutral creature
"dimensionDoorTriggersGuards" : false,
"dimensionDoorTriggersGuards" : true,
// if enabled, dimension door can be used 1x per day, exception being 2x per day for XL+U or bigger maps (41472 tiles) + hero having expert air magic
"dimensionDoorTournamentRulesLimit" : false
},

View File

@ -287,22 +287,9 @@ std::vector<const CGObjectInstance*> CGameInfoCallback::getGuardingCreatures (in
return ret;
}
bool CGameInfoCallback::isTileGuardedAfterDimensionDoorUse(int3 tile, const CGHeroInstance * castingHero) const
bool CGameInfoCallback::isTileGuardedUnchecked(int3 tile) const
{
//for known tiles this is just potential convenience info, for tiles behind fog of war this info matches HotA but not H3 so make it accessible only with proper setting on
bool canAccessInfo = false;
if(isVisible(tile))
canAccessInfo = true;
else if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS)
&& isInScreenRange(castingHero->getSightCenter(), tile)
&& castingHero->canCastThisSpell(static_cast<SpellID>(SpellID::DIMENSION_DOOR).toSpell()))
canAccessInfo = true; //TODO: check if available casts > 0, before adding that check make dimension door daily limit popup trigger on spell pick
if(canAccessInfo)
return !gs->guardingCreatures(tile).empty();
return false;
}
bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero & dest, const CGObjectInstance * selectedObject) const
@ -528,40 +515,12 @@ const TerrainTile * CGameInfoCallback::getTile(int3 tile, bool verbose) const
return nullptr;
}
const TerrainTile * CGameInfoCallback::getTileForDimensionDoor(int3 tile, const CGHeroInstance * castingHero) const
const TerrainTile * CGameInfoCallback::getTileUnchecked(int3 tile) const
{
auto outputTile = getTile(tile, false);
if (isInTheMap(tile))
return &gs->map->getTile(tile);
if(outputTile != nullptr)
return outputTile;
bool allowOnlyToUncoveredTiles = VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES);
if(!allowOnlyToUncoveredTiles)
{
if(castingHero->canCastThisSpell(static_cast<SpellID>(SpellID::DIMENSION_DOOR).toSpell())
&& isInScreenRange(castingHero->getSightCenter(), tile))
{ //TODO: check if available casts > 0, before adding that check make dimension door daily limit popup trigger on spell pick
//we are allowed to get basic blocked/water invisible nearby tile date when casting DD spell
TerrainTile targetTile = gs->map->getTile(tile);
auto obfuscatedTile = std::make_shared<TerrainTile>();
obfuscatedTile->visitable = false;
obfuscatedTile->blocked = targetTile.blocked || targetTile.visitable;
if(targetTile.blocked || targetTile.visitable)
obfuscatedTile->terType = VLC->terrainTypeHandler->getById(TerrainId::ROCK);
else if(!VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE))
obfuscatedTile->terType = gs->map->getTile(castingHero->getSightCenter()).terType;
else
obfuscatedTile->terType = targetTile.isWater()
? VLC->terrainTypeHandler->getById(TerrainId::WATER)
: VLC->terrainTypeHandler->getById(TerrainId::GRASS);
outputTile = obfuscatedTile.get();
}
}
return outputTile;
return nullptr;
}
EDiggingStatus CGameInfoCallback::getTileDigStatus(int3 tile, bool verbose) const

View File

@ -193,11 +193,11 @@ public:
//map
virtual int3 guardingCreaturePosition (int3 pos) const;
virtual std::vector<const CGObjectInstance*> getGuardingCreatures (int3 pos) const;
virtual bool isTileGuardedAfterDimensionDoorUse(int3 tile, const CGHeroInstance * castingHero) const;
virtual bool isTileGuardedUnchecked(int3 tile) const;
virtual const CMapHeader * getMapHeader()const;
virtual int3 getMapSize() const; //returns size of map - z is 1 for one - level map and 2 for two level map
virtual const TerrainTile * getTile(int3 tile, bool verbose = true) const;
virtual const TerrainTile * getTileForDimensionDoor(int3 tile, const CGHeroInstance * castingHero) const;
virtual const TerrainTile * getTileUnchecked(int3 tile) const;
virtual std::shared_ptr<const boost::multi_array<TerrainTile*, 3>> getAllVisibleTiles() const;
virtual bool isInTheMap(const int3 &pos) const;
virtual void getVisibleTilesInRange(std::unordered_set<int3> &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const;

View File

@ -105,6 +105,7 @@ void GameSettings::load(const JsonNode & input)
{EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" },
{EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles"},
{EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" },
{EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" },
{EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" },
{EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit"},
{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" },

View File

@ -72,6 +72,7 @@ enum class EGameSettings
COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,
DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES,
DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE,
DIMENSION_DOOR_FAILURE_SPENDS_POINTS,
DIMENSION_DOOR_TRIGGERS_GUARDS,
DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,

View File

@ -13,6 +13,7 @@
#include "AdventureSpellMechanics.h"
#include "CSpellHandler.h"
#include "Problem.h"
#include "../CGameInfoCallback.h"
#include "../CPlayerState.h"
@ -32,37 +33,52 @@ AdventureSpellMechanics::AdventureSpellMechanics(const CSpell * s):
{
}
bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
bool AdventureSpellMechanics::canBeCast(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const
{
if(!owner->isAdventure())
{
env->complain("Attempt to cast non adventure spell in adventure mode");
return false;
}
if(const CGHeroInstance * heroCaster = dynamic_cast<const CGHeroInstance *>(parameters.caster))
const auto * heroCaster = dynamic_cast<const CGHeroInstance *>(caster);
if (heroCaster)
{
if(heroCaster->inTownGarrison)
{
env->complain("Attempt to cast an adventure spell in town garrison");
return false;
}
const auto level = heroCaster->getSpellSchoolLevel(owner);
const auto cost = owner->getCost(level);
if(!heroCaster->canCastThisSpell(owner))
{
env->complain("Hero cannot cast this spell!");
return false;
if(heroCaster->mana < cost)
return false;
}
if(heroCaster->mana < cost)
{
env->complain("Hero doesn't have enough spell points to cast this spell!");
return canBeCastImpl(problem, cb, caster);
}
bool AdventureSpellMechanics::canBeCastAt(spells::Problem & problem, const CGameInfoCallback * 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 CGameInfoCallback * cb, const spells::Caster * caster) const
{
return true;
}
bool AdventureSpellMechanics::canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
return true;
}
bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
spells::detail::ProblemImpl problem;
if (!canBeCastAt(problem, env->getCb(), parameters.caster, parameters.pos))
return false;
}
}
ESpellCastResult result = beginCast(env, parameters);
@ -138,35 +154,34 @@ SummonBoatMechanics::SummonBoatMechanics(const CSpell * s):
{
}
ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
bool SummonBoatMechanics::canBeCastImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const
{
if(!parameters.caster->getHeroCaster())
if(!caster->getHeroCaster())
return false;
if(caster->getHeroCaster()->boat)
{
env->complain("Not a hero caster!");
return ESpellCastResult::ERROR;
MetaString message = MetaString::createFromTextID("core.genrltxt.333");
caster->getCasterName(message);
problem.add(std::move(message));
return false;
}
if(parameters.caster->getHeroCaster()->boat)
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
iw.text.appendLocalString(EMetaText::GENERAL_TXT, 333);//%s is already in boat
parameters.caster->getCasterName(iw.text);
env->apply(&iw);
return ESpellCastResult::CANCEL;
}
int3 summonPos = parameters.caster->getHeroCaster()->bestLocation();
int3 summonPos = caster->getHeroCaster()->bestLocation();
if(summonPos.x < 0)
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
iw.text.appendLocalString(EMetaText::GENERAL_TXT, 334);//There is no place to put the boat.
env->apply(&iw);
return ESpellCastResult::CANCEL;
MetaString message = MetaString::createFromTextID("core.genrltxt.334");
caster->getCasterName(message);
problem.add(std::move(message));
return false;
}
return true;
}
ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
//check if spell works at all
@ -200,6 +215,8 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
}
}
int3 summonPos = parameters.caster->getHeroCaster()->bestLocation();
if(nullptr != nearest) //we found boat to summon
{
ChangeObjPos cop;
@ -214,6 +231,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
iw.player = parameters.caster->getCasterOwner();
iw.text.appendLocalString(EMetaText::GENERAL_TXT, 335); //There are no boats to summon.
env->apply(&iw);
return ESpellCastResult::ERROR;
}
else //create boat
{
@ -233,6 +251,29 @@ ScuttleBoatMechanics::ScuttleBoatMechanics(const CSpell * s):
{
}
bool ScuttleBoatMechanics::canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * 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->isVisible(pos, caster->getCasterOwner()))
return false;
const TerrainTile * t = cb->getTile(pos);
if(!t || t->visitableObjects.empty() || t->visitableObjects.back()->ID != Obj::BOAT)
return false;
return true;
}
ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
{
const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
@ -247,36 +288,11 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen
return ESpellCastResult::OK;
}
if(!env->getMap()->isInTheMap(parameters.pos))
{
env->complain("Invalid dst tile for scuttle!");
return ESpellCastResult::ERROR;
}
int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter();
if(!isInScreenRange(casterPosition, parameters.pos))
{
env->complain("Attempting to cast Scuttle Boat outside screen range!");
return ESpellCastResult::ERROR;
}
if(!env->getCb()->isVisible(parameters.pos, parameters.caster->getCasterOwner()))
{
env->complain("Attempting to cast Scuttle Boat at invisible tile!");
return ESpellCastResult::ERROR;
}
const TerrainTile *t = &env->getMap()->getTile(parameters.pos);
if(t->visitableObjects.empty() || t->visitableObjects.back()->ID != Obj::BOAT)
{
env->complain("There is no boat to scuttle!");
return ESpellCastResult::ERROR;
}
const TerrainTile & t = env->getMap()->getTile(parameters.pos);
RemoveObject ro;
ro.initiator = parameters.caster->getCasterOwner();
ro.objectID = t->visitableObjects.back()->id;
ro.objectID = t.visitableObjects.back()->id;
env->apply(&ro);
return ESpellCastResult::OK;
}
@ -287,71 +303,31 @@ DimensionDoorMechanics::DimensionDoorMechanics(const CSpell * s):
{
}
ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
bool DimensionDoorMechanics::canBeCastImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const
{
if(!env->getMap()->isInTheMap(parameters.pos))
if(!caster->getHeroCaster())
return false;
if(caster->getHeroCaster()->movementPointsRemaining() <= 0) //unlike town portal non-zero MP is enough
{
env->complain("Destination is out of map!");
return ESpellCastResult::ERROR;
problem.add(MetaString::createFromTextID("core.genrltxt.125"));
return false;
}
if(!parameters.caster->getHeroCaster())
{
env->complain("Not a hero caster!");
return ESpellCastResult::ERROR;
}
int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter();
const TerrainTile * dest = env->getCb()->getTile(parameters.pos);
const TerrainTile * curr = env->getCb()->getTile(casterPosition);
if(nullptr == dest)
{
env->complain("Destination tile doesn't exist!");
return ESpellCastResult::ERROR;
}
if(nullptr == curr)
{
env->complain("Source tile doesn't exist!");
return ESpellCastResult::ERROR;
}
if(parameters.caster->getHeroCaster()->movementPointsRemaining() <= 0) //unlike town portal non-zero MP is enough
{
env->complain("Hero needs movement points to cast Dimension Door!");
return ESpellCastResult::ERROR;
}
if(!isInScreenRange(casterPosition, parameters.pos))
{
env->complain("Attempting to cast Dimension Door outside screen range!");
return ESpellCastResult::ERROR;
}
if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES))
{
if(!env->getCb()->isVisible(parameters.pos, parameters.caster->getCasterOwner()))
{
env->complain("Attempting to cast Dimension Door inside Fog of War with limitation toggled on!");
return ESpellCastResult::ERROR;
}
}
const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
const int movementCost = GameConstants::BASE_MOVEMENT_COST * ((schoolLevel >= 3) ? 2 : 3);
const auto schoolLevel = caster->getSpellSchoolLevel(owner);
std::stringstream cachingStr;
cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num;
int castsAlreadyPerformedThisTurn = parameters.caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), Selector::all, cachingStr.str())->size();
int castsAlreadyPerformedThisTurn = caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), Selector::all, cachingStr.str())->size();
int castsLimit = owner->getLevelPower(schoolLevel);
bool isTournamentRulesLimitEnabled = VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT);
if(isTournamentRulesLimitEnabled)
{
bool meetsTournamentRulesTwoCastsRequirements = env->getMap()->width * env->getMap()->height * env->getMap()->levels() >= GameConstants::TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD
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;
@ -359,31 +335,90 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
if(castsAlreadyPerformedThisTurn >= castsLimit) //limit casts per turn
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();
iw.text.appendLocalString(EMetaText::GENERAL_TXT, 338); //%s is not skilled enough to cast this spell again today.
parameters.caster->getCasterName(iw.text);
env->apply(&iw);
return ESpellCastResult::CANCEL;
MetaString message = MetaString::createFromTextID("core.genrltxt.338");
caster->getCasterName(message);
problem.add(std::move(message));
return false;
}
if(!dest->isClear(curr)) //wrong dest tile
return true;
}
bool DimensionDoorMechanics::canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const
{
if(!cb->isInTheMap(pos))
return false;
if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES))
{
if(!cb->isVisible(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(VLC->settings()->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 movementCost = GameConstants::BASE_MOVEMENT_COST * ((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();
iw.text.appendLocalString(EMetaText::GENERAL_TXT, 70); //Dimension Door failed!
// tile is either blocked or not possible to move (e.g. water <-> land)
if(VLC->settings()->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);
if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), true))
{
SetMovePoints smp;
smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId());
if(movementCost < static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()))
@ -391,7 +426,10 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
else
smp.val = 0;
env->apply(&smp);
}
if(dest->isClear(curr))
env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), true);
return ESpellCastResult::OK;
}

View File

@ -24,45 +24,58 @@ enum class ESpellCastResult
ERROR//internal error occurred
};
class DLL_LINKAGE AdventureSpellMechanics : public IAdventureSpellMechanics
class AdventureSpellMechanics : public IAdventureSpellMechanics
{
public:
AdventureSpellMechanics(const CSpell * s);
bool canBeCast(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const override final;
bool canBeCastAt(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override 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 bool canBeCastImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const;
virtual bool canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const;
void performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const ESpellCastResult result) const;
};
class DLL_LINKAGE SummonBoatMechanics : public AdventureSpellMechanics
class SummonBoatMechanics final : public AdventureSpellMechanics
{
public:
SummonBoatMechanics(const CSpell * s);
protected:
bool canBeCastImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const override;
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
};
class DLL_LINKAGE ScuttleBoatMechanics : public AdventureSpellMechanics
class ScuttleBoatMechanics final : public AdventureSpellMechanics
{
public:
ScuttleBoatMechanics(const CSpell * s);
protected:
bool canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override;
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
};
class DLL_LINKAGE DimensionDoorMechanics : public AdventureSpellMechanics
class DimensionDoorMechanics final : public AdventureSpellMechanics
{
public:
DimensionDoorMechanics(const CSpell * s);
protected:
bool canBeCastImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const override;
bool canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override;
ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
};
class DLL_LINKAGE TownPortalMechanics : public AdventureSpellMechanics
class TownPortalMechanics final : public AdventureSpellMechanics
{
public:
TownPortalMechanics(const CSpell * s);
@ -75,7 +88,7 @@ private:
std::vector <const CGTownInstance*> getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
};
class DLL_LINKAGE ViewMechanics : public AdventureSpellMechanics
class ViewMechanics : public AdventureSpellMechanics
{
public:
ViewMechanics(const CSpell * s);
@ -85,7 +98,7 @@ protected:
virtual bool showTerrain(const int32_t spellLevel) const = 0;
};
class DLL_LINKAGE ViewAirMechanics : public ViewMechanics
class ViewAirMechanics final : public ViewMechanics
{
public:
ViewAirMechanics(const CSpell * s);
@ -94,7 +107,7 @@ protected:
bool showTerrain(const int32_t spellLevel) const override;
};
class DLL_LINKAGE ViewEarthMechanics : public ViewMechanics
class ViewEarthMechanics final : public ViewMechanics
{
public:
ViewEarthMechanics(const CSpell * s);

View File

@ -491,6 +491,11 @@ void CSpell::setupMechanics()
adventureMechanics = IAdventureSpellMechanics::createMechanics(this);
}
const IAdventureSpellMechanics & CSpell::getAdventureMechanics() const
{
return *adventureMechanics;
}
std::unique_ptr<spells::Mechanics> CSpell::battleMechanics(const spells::IBattleCast * event) const
{
return mechanics->create(event);

View File

@ -290,6 +290,7 @@ public://internal, for use only by Mechanics classes
///returns raw damage or healed HP
int64_t calculateRawEffectValue(int32_t effectLevel, int32_t basePowerMultiplier, int32_t levelPowerMultiplier) const;
const IAdventureSpellMechanics & getAdventureMechanics() const;
std::unique_ptr<spells::Mechanics> battleMechanics(const spells::IBattleCast * event) const;
private:
void setIsOffensive(const bool val);

View File

@ -355,6 +355,9 @@ public:
IAdventureSpellMechanics(const CSpell * s);
virtual ~IAdventureSpellMechanics() = default;
virtual bool canBeCast(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const = 0;
virtual bool canBeCastAt(spells::Problem & problem, const CGameInfoCallback * 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);