1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-01-26 03:52:01 +02:00

Merge pull request #3505 from IvanSavenko/bugfixing

[1.4.3] Bugfixing
This commit is contained in:
Ivan Savenko 2024-01-16 22:00:11 +02:00 committed by GitHub
commit bf62e47481
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 124 additions and 38 deletions

View File

@ -405,7 +405,10 @@ std::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(const
std::shared_ptr<CPlayerBattleCallback> CBattleCallback::getBattle(const BattleID & battleID)
{
return activeBattles.at(battleID);
if (activeBattles.count(battleID))
return activeBattles.at(battleID);
throw std::runtime_error("Failed to find battle " + std::to_string(battleID.getNum()) + " of player " + player->toString() + ". Number of ongoing battles: " + std::to_string(activeBattles.size()));
}
std::optional<PlayerColor> CBattleCallback::getPlayerID() const
@ -415,10 +418,18 @@ std::optional<PlayerColor> CBattleCallback::getPlayerID() const
void CBattleCallback::onBattleStarted(const IBattleInfo * info)
{
if (activeBattles.count(info->getBattleID()) > 0)
throw std::runtime_error("Player " + player->toString() + " is already engaged in battle " + std::to_string(info->getBattleID().getNum()));
logGlobal->debug("Battle %d started for player %s", info->getBattleID(), player->toString());
activeBattles[info->getBattleID()] = std::make_shared<CPlayerBattleCallback>(info, *getPlayerID());
}
void CBattleCallback::onBattleEnded(const BattleID & battleID)
{
if (activeBattles.count(battleID) == 0)
throw std::runtime_error("Player " + player->toString() + " is not engaged in battle " + std::to_string(battleID.getNum()));
logGlobal->debug("Battle %d ended for player %s", battleID, player->toString());
activeBattles.erase(battleID);
}

View File

@ -560,18 +560,16 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
void CClient::battleStarted(const BattleInfo * info)
{
std::shared_ptr<CPlayerInterface> att, def;
auto & leftSide = info->sides[0];
auto & rightSide = info->sides[1];
for(auto & battleCb : battleCallbacks)
{
if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; })
|| !battleCb.first.isValidPlayer())
{
if(!battleCb.first.isValidPlayer() || battleCb.first == leftSide.color || battleCb.first == rightSide.color)
battleCb.second->onBattleStarted(info);
}
}
std::shared_ptr<CPlayerInterface> att, def;
auto & leftSide = info->sides[0], & rightSide = info->sides[1];
//If quick combat is not, do not prepare interfaces for battleint
auto callBattleStart = [&](PlayerColor color, ui8 side)
{

View File

@ -202,6 +202,7 @@ public:
bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;};
void giveHeroBonus(GiveBonus * bonus) override {};
void setMovePoints(SetMovePoints * smp) override {};
void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override {};
void setManaPoints(ObjectInstanceID hid, int val) override {};
void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {};
void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {};

View File

@ -691,7 +691,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
if(pathNode->layer == EPathfindingLayer::LAND)
CCS->curh->set(cursorMove[turns]);
else
CCS->curh->set(cursorSailVisit[turns]);
CCS->curh->set(cursorSail[turns]);
break;
case EPathNodeAction::VISIT:
@ -706,6 +706,15 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
}
else if(pathNode->layer == EPathfindingLayer::LAND)
CCS->curh->set(cursorVisit[turns]);
else if (pathNode->layer == EPathfindingLayer::SAIL &&
objAtTile &&
objAtTile->isCoastVisitable() &&
pathNode->theNodeBefore &&
pathNode->theNodeBefore->layer == EPathfindingLayer::LAND )
{
// exception - when visiting shipwreck located on coast from land - show 'horse' cursor, not 'ship' cursor
CCS->curh->set(cursorVisit[turns]);
}
else
CCS->curh->set(cursorSailVisit[turns]);
break;

View File

@ -411,12 +411,6 @@
"type" : "LEVEL_SPELL_IMMUNITY",
"val" : 5
},
"hateGiants" :
{
"type" : "HATE",
"subtype" : "creature.giant",
"val" : 50
},
"hateTitans" :
{
"type" : "HATE",

View File

@ -61,6 +61,10 @@
"positive": true,
},
// If true, then creature capable of casting this spell can cast this spell on itself
// If false, then creature can only cast this spell on other units
"canCastOnSelf" : false,
// If true, spell won't be available on a map without water
"onlyOnWaterMap" : true,

View File

@ -185,7 +185,7 @@ Following options can be used to configure simultaneous turns:
While simultaneous turns are active, VCMI tracks contacts for each pair of player separately.
Players are considered to be "in contact" if movement range of their heroes at the start of turn overlaps, or, in other words - if their heroes can meet on this turn if both walk towards each other. When calculating movement range, game uses same rules as standard movement range calculation in vcmi, meaning that game will track movement through monoliths and subterranean gates, but will not account for any removable obstacles, such as wandering monsters or treasures that block path between heroes. At the moment, game will not account for any ways to extend movement range - Dimension Door or Town Portal spells, visiting map objects such as Stables, releasing heroes from prisons, etc.
Players are considered to be "in contact" if movement range of their heroes at the start of turn overlaps, or, in other words - if their heroes can meet on this turn if both walk towards each other. When calculating movement range, game uses rules similar to standard movement range calculation in vcmi, meaning that game will track movement through monoliths and subterranean gates, but will not account for any removable obstacles, such as pickable treasures that block path between heroes. Any existing wandering monsters that block path between heroes are ignored for range calculation. At the moment, game will not account for any ways to extend movement range - Dimension Door or Town Portal spells, visiting map objects such as Stables, releasing heroes from prisons, etc.
Once detected, contact can never be "lost". If game detected contact between two players, this contact will remain active till the end of the game, even if their heroes move far enough from each other.

View File

@ -44,6 +44,7 @@ public:
virtual bool isMagical() const = 0; //Should this spell considered as magical effect or as ability (like dendroid's bind)
virtual bool hasSchool(SpellSchool school) const = 0;
virtual bool canCastOnSelf() const = 0;
virtual void forEachSchool(const SchoolCallback & cb) const = 0;
virtual int32_t getCost(const int32_t skillLevel) const = 0;

View File

@ -397,7 +397,10 @@ void CCreature::serializeJson(JsonSerializeFormat & handler)
if(!handler.saving)
{
if(ammMin > ammMax)
{
logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier);
std::swap(ammMin, ammMax);
}
}
}
@ -622,7 +625,7 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json
}
else
{
logGlobal->error("Mod %s: creature %s has minimal damage (%d) greater than maximal damage (%d)!", scope, identifier, minDamage, maxDamage);
logMod->error("Mod %s: creature %s has minimal damage (%d) greater than maximal damage (%d)!", scope, identifier, minDamage, maxDamage);
cre->addBonus(maxDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin);
cre->addBonus(minDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax);
}

View File

@ -473,7 +473,11 @@ void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const
hero->initialArmy[i].minAmount = static_cast<ui32>(source["min"].Float());
hero->initialArmy[i].maxAmount = static_cast<ui32>(source["max"].Float());
assert(hero->initialArmy[i].minAmount <= hero->initialArmy[i].maxAmount);
if (hero->initialArmy[i].minAmount > hero->initialArmy[i].maxAmount)
{
logMod->error("Hero %s has minimal army size (%d) greater than maximal size (%d)!", hero->getJsonKey(), hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount);
std::swap(hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount);
}
VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature)
{

View File

@ -37,21 +37,25 @@ void CRandomGenerator::resetSeed()
TRandI CRandomGenerator::getIntRange(int lower, int upper)
{
assert(lower <= upper);
return std::bind(TIntDist(lower, upper), std::ref(rand));
}
vstd::TRandI64 CRandomGenerator::getInt64Range(int64_t lower, int64_t upper)
{
assert(lower <= upper);
return std::bind(TInt64Dist(lower, upper), std::ref(rand));
}
int CRandomGenerator::nextInt(int upper)
{
assert(0 <= upper);
return getIntRange(0, upper)();
}
int CRandomGenerator::nextInt(int lower, int upper)
{
assert(lower <= upper);
return getIntRange(lower, upper)();
}
@ -62,16 +66,19 @@ int CRandomGenerator::nextInt()
vstd::TRand CRandomGenerator::getDoubleRange(double lower, double upper)
{
return std::bind(TRealDist(lower, upper), std::ref(rand));
assert(lower <= upper);
return std::bind(TRealDist(lower, upper), std::ref(rand));
}
double CRandomGenerator::nextDouble(double upper)
{
assert(0 <= upper);
return getDoubleRange(0, upper)();
}
double CRandomGenerator::nextDouble(double lower, double upper)
{
assert(lower <= upper);
return getDoubleRange(lower, upper)();
}

View File

@ -122,6 +122,7 @@ public:
virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0;
virtual void giveHeroBonus(GiveBonus * bonus)=0;
virtual void setMovePoints(SetMovePoints * smp)=0;
virtual void setMovePoints(ObjectInstanceID hid, int val, bool absolute)=0;
virtual void setManaPoints(ObjectInstanceID hid, int val)=0;
virtual void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) = 0;
virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0;

View File

@ -210,17 +210,21 @@ void CGameStateCampaign::placeCampaignHeroes()
// with the same hero type id
std::vector<CGHeroInstance *> removedHeroes;
std::set<HeroTypeID> heroesToRemove = campaignState->getReservedHeroes();
std::set<HeroTypeID> reservedHeroes = campaignState->getReservedHeroes();
std::set<HeroTypeID> heroesToRemove;
for (auto const & heroID : reservedHeroes )
{
// Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear
if (!campaignState->getHeroByType(heroID).isNull())
heroesToRemove.insert(heroID);
}
for(auto & campaignHeroReplacement : campaignHeroReplacements)
heroesToRemove.insert(campaignHeroReplacement.hero->getHeroType());
for(auto & heroID : heroesToRemove)
{
// Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear
if (campaignState->getHeroByType(heroID).isNull())
continue;
auto * hero = gameState->getUsedHero(heroID);
if(hero)
{

View File

@ -40,7 +40,7 @@ TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const
return TavernSlotRole::NONE;
}
void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role)
void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints)
{
vstd::erase_if(currentTavern, [&](const TavernSlot & entry){
return entry.player == player && entry.slot == slot;
@ -54,6 +54,12 @@ void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot,
if (h && army)
h->setToArmy(army);
if (h && replenishPoints)
{
h->setMovementPoints(h->movementPointsLimit(true));
h->mana = h->manaLimit();
}
TavernSlot newSlot;
newSlot.hero = h;
newSlot.player = player;

View File

@ -74,7 +74,7 @@ public:
void setAvailability(HeroTypeID hero, std::set<PlayerColor> mask);
/// Makes hero available in tavern of specified player
void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role);
void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints);
template <typename Handler> void serialize(Handler &h, const int version)
{

View File

@ -436,14 +436,14 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst)
int count = rand.nextInt(stack.minAmount, stack.maxAmount);
const CCreature * creature = stack.creature.toCreature();
if(creature == nullptr)
if(stack.creature == CreatureID::NONE)
{
logGlobal->error("Hero %s has invalid creature with id %d in initial army", getNameTranslated(), stack.creature.toEnum());
logGlobal->error("Hero %s has invalid creature in initial army", getNameTranslated());
continue;
}
const CCreature * creature = stack.creature.toCreature();
if(creature->warMachine != ArtifactID::NONE) //war machine
{
warMachinesGiven++;

View File

@ -151,11 +151,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const
gb.id = heroID;
cb->giveHeroBonus(&gb);
SetMovePoints mp;
mp.val = 600;
mp.absolute = false;
mp.hid = heroID;
cb->setMovePoints(&mp);
cb->setMovePoints(heroID, 600, false);
iw.text.appendRawString(VLC->generaltexth->allTexts[580]);
cb->showInfoDialog(&iw);

View File

@ -959,7 +959,7 @@ void FoWChange::applyGs(CGameState *gs)
void SetAvailableHero::applyGs(CGameState *gs)
{
gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID);
gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID, replenishPoints);
}
void GiveBonus::applyGs(CGameState *gs)

View File

@ -352,6 +352,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient
PlayerColor player;
HeroTypeID hid; //HeroTypeID::NONE if no hero
CSimpleArmy army;
bool replenishPoints;
void visitTyped(ICPackVisitor & visitor) override;
@ -362,6 +363,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient
h & player;
h & hid;
h & army;
h & replenishPoints;
}
};

View File

@ -213,7 +213,24 @@ bool BattleSpellMechanics::canBeCastAt(const Target & target, Problem & problem)
Target spellTarget = transformSpellTarget(target);
return effects->applicable(problem, this, target, spellTarget);
const battle::Unit * mainTarget = nullptr;
if (!getSpell()->canCastOnSelf())
{
if(spellTarget.front().unitValue)
{
mainTarget = target.front().unitValue;
}
else if(spellTarget.front().hexValue.isValid())
{
mainTarget = battle()->battleGetUnitByPos(target.front().hexValue, true);
}
if (mainTarget && mainTarget == caster)
return false; // can't cast on self
}
return effects->applicable(problem, this, target, spellTarget);
}
std::vector<const CStack *> BattleSpellMechanics::getAffectedStacks(const Target & target) const

View File

@ -76,6 +76,7 @@ CSpell::CSpell():
power(0),
combat(false),
creatureAbility(false),
castOnSelf(false),
positiveness(ESpellPositiveness::NEUTRAL),
defaultProbability(0),
rising(false),
@ -285,6 +286,11 @@ bool CSpell::hasBattleEffects() const
return levels[0].battleEffects.getType() == JsonNode::JsonType::DATA_STRUCT && !levels[0].battleEffects.Struct().empty();
}
bool CSpell::canCastOnSelf() const
{
return castOnSelf;
}
const std::string & CSpell::getIconImmune() const
{
return iconImmune;
@ -702,6 +708,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode &
spell->school[info.id] = schoolNames[info.jsonName].Bool();
}
spell->castOnSelf = json["canCastOnSelf"].Bool();
spell->level = static_cast<si32>(json["level"].Integer());
spell->power = static_cast<si32>(json["power"].Integer());

View File

@ -203,6 +203,7 @@ public:
int64_t calculateDamage(const spells::Caster * caster) const override;
bool hasSchool(SpellSchool school) const override;
bool canCastOnSelf() const override;
/**
* Calls cb for each school this spell belongs to
@ -329,6 +330,7 @@ private:
si32 power; //spell's power
bool combat; //is this spell combat (true) or adventure (false)
bool creatureAbility; //if true, only creatures can use this spell
bool castOnSelf; // if set, creature caster can cast this spell on itself
si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative
std::unique_ptr<spells::ISpellMechanicsFactory> mechanics;//(!) do not serialize

View File

@ -169,7 +169,6 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg
case 1:
//Coronius style specialty bonus.
//Please note that actual Coronius isnt here, because Slayer is a spell that doesnt affect monster stats and is used only in calculateDmgRange
power = std::max(5 - tier, 0);
break;
}
if(m->isNegativeSpell())

View File

@ -1467,6 +1467,9 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta
sendAndApply(&vc);
visitCastleObjects(obj, hero);
giveSpells (obj, hero);
if (obj->visitingHero && obj->garrisonHero)
useScholarSkill(obj->visitingHero->id, obj->garrisonHero->id);
checkVictoryLossConditionsForPlayer(hero->tempOwner); //transported artifact?
}
@ -1510,6 +1513,15 @@ void CGameHandler::setMovePoints(SetMovePoints * smp)
sendAndApply(smp);
}
void CGameHandler::setMovePoints(ObjectInstanceID hid, int val, bool absolute)
{
SetMovePoints smp;
smp.hid = hid;
smp.val = val;
smp.absolute = absolute;
sendAndApply(&smp);
}
void CGameHandler::setManaPoints(ObjectInstanceID hid, int val)
{
SetMana sm;

View File

@ -142,6 +142,7 @@ public:
bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override;
void giveHeroBonus(GiveBonus * bonus) override;
void setMovePoints(SetMovePoints * smp) override;
void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override;
void setManaPoints(ObjectInstanceID hid, int val) override;
void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override;
void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override;

View File

@ -74,6 +74,7 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer
sah.slotID = selectSlotForRole(color, sah.roleID);
sah.player = color;
sah.hid = hero->getHeroType();
sah.replenishPoints = false;
gameHandler->sendAndApply(&sah);
}
@ -87,6 +88,7 @@ void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroIns
sah.hid = hero->getHeroType();
sah.army.clearSlots();
sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1);
sah.replenishPoints = false;
gameHandler->sendAndApply(&sah);
}
@ -98,6 +100,7 @@ void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroS
sah.roleID = TavernSlotRole::NONE;
sah.slotID = slot;
sah.hid = HeroTypeID::NONE;
sah.replenishPoints = false;
gameHandler->sendAndApply(&sah);
}
@ -106,6 +109,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe
SetAvailableHero sah;
sah.player = color;
sah.slotID = slot;
sah.replenishPoints = true;
CGHeroInstance *newHero = pickHeroFor(needNativeHero, color);
@ -129,6 +133,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe
{
sah.hid = HeroTypeID::NONE;
}
gameHandler->sendAndApply(&sah);
}

View File

@ -82,6 +82,7 @@ public:
bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}
void giveHeroBonus(GiveBonus * bonus) override {}
void setMovePoints(SetMovePoints * smp) override {}
void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override {};
void setManaPoints(ObjectInstanceID hid, int val) override {}
void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}
void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}

View File

@ -45,6 +45,7 @@ public:
MOCK_CONST_METHOD0(isOffensive, bool());
MOCK_CONST_METHOD0(isSpecial, bool());
MOCK_CONST_METHOD0(isMagical, bool());
MOCK_CONST_METHOD0(canCastOnSelf, bool());
MOCK_CONST_METHOD1(hasSchool, bool(SpellSchool));
MOCK_CONST_METHOD1(forEachSchool, void(const SchoolCallback &));
MOCK_CONST_METHOD0(getCastSound, const std::string &());