From 4b7b67d411b6372200934f7a34c62c3a594f0cee Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Oct 2025 11:47:23 +0200 Subject: [PATCH 01/10] Add description to MORE_DAMAGE_FROM_SPELL bonus --- Mods/vcmi/Content/config/english.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index 82fb3b677..7b62549f2 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -1016,6 +1016,7 @@ "core.bonus.MANA_DRAIN.description" : "{Drains enemy mana}\nDrains ${val} mana every turn from enemy hero", "core.bonus.MECHANICAL.description" : "{Mechanical}\nThis unit is immune to effects that only affect living and can be repaired", "core.bonus.MIND_IMMUNITY.description" : "{Mind Spell Immunity}\nThis unit cannot be targeted by spells that affect its mind", + "core.bonus.MORE_DAMAGE_FROM_SPELL.description" : "{Vulnerable to ${subtype.spell}}\nThe damage taken by this unit when hit by a ${subtype.spell} is increased by ${val}%", "core.bonus.NO_DISTANCE_PENALTY.description" : "{No distance penalty}\nRanged attacks deal full damage at any distance", "core.bonus.NO_MELEE_PENALTY.description" : "{No melee penalty}\nThis ranged unit deals full damage with melee attacks", "core.bonus.NO_MORALE.description" : "{Neutral Morale}\nCreature is immune to morale effects", From 34a4668998c9dd0cb01539e9d5ef85f46931484c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Oct 2025 11:47:53 +0200 Subject: [PATCH 02/10] Do not require frameAngles for ray-based animation --- client/battle/BattleProjectileController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/battle/BattleProjectileController.cpp b/client/battle/BattleProjectileController.cpp index df8d0211e..a37dc1b03 100644 --- a/client/battle/BattleProjectileController.cpp +++ b/client/battle/BattleProjectileController.cpp @@ -161,7 +161,7 @@ const CCreature & BattleProjectileController::getShooter(const CStack * stack) c if(creature->getId() == CreatureID::ARROW_TOWERS) creature = owner.siegeController->getTurretCreature(stack->initialPosition); - if(creature->animation.missileFrameAngles.empty()) + if(creature->animation.missileFrameAngles.empty() && creature->animation.projectileRay.empty()) { logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead...", creature->getNameSingularTranslated()); creature = CreatureID(CreatureID::ARCHER).toCreature(); From 152e7ed74b7e78a03782c736a3bdb8bfdf19a1cd Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Oct 2025 11:49:51 +0200 Subject: [PATCH 03/10] Fix missing descriptions for creature abilities spells --- lib/spells/CSpellHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 00fb835a5..122caf768 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -953,7 +953,7 @@ std::shared_ptr CSpellHandler::loadFromJson(const std::string & scope, c const si32 levelPower = levelObject.power = static_cast(levelNode["power"].Integer()); - if (!spell->isCreatureAbility()) + if (!levelNode["description"].String().empty()) LIBRARY->generaltexth->registerString(scope, spell->getDescriptionTextID(levelIndex), levelNode["description"]); levelObject.cost = static_cast(levelNode["cost"].Integer()); From 8e747976502461390ac63c756cf666c495a47bd5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Oct 2025 11:57:20 +0200 Subject: [PATCH 04/10] Remove hardcoded checks for Conflux and Inferno Grails --- config/factions/conflux.json | 2 +- config/factions/inferno.json | 2 +- lib/constants/Enumerations.h | 4 ++- lib/constants/StringConstants.h | 4 ++- lib/mapObjects/CGTownInstance.cpp | 7 ------ lib/mapObjects/CGTownInstance.h | 1 - server/CGameHandler.cpp | 34 ++++++++++++-------------- server/processors/NewTurnProcessor.cpp | 2 +- 8 files changed, 24 insertions(+), 32 deletions(-) diff --git a/config/factions/conflux.json b/config/factions/conflux.json index b9ef8f020..27140d243 100644 --- a/config/factions/conflux.json +++ b/config/factions/conflux.json @@ -196,7 +196,7 @@ "earthMagic" ] }, - "grail": { }, + "grail": { "type" : "auroraBorealis" }, "extraTownHall": { }, "extraCityHall": { }, "extraCapitol": { }, diff --git a/config/factions/inferno.json b/config/factions/inferno.json index e6fd10f10..07ee66bbf 100644 --- a/config/factions/inferno.json +++ b/config/factions/inferno.json @@ -205,7 +205,7 @@ }, "horde2": { "upgrades" : "dwellingLvl3" }, "horde2Upgr": { "upgrades" : "dwellingUpLvl3", "requires" : [ "horde2" ] }, - "grail": { }, + "grail": { "type" : "deityOfFire" }, "dwellingLvl1": { "requires" : [ "fort" ] }, "dwellingLvl2": { "requires" : [ "dwellingLvl1" ] }, diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index 952f7b64f..303b55f38 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -31,7 +31,9 @@ namespace BuildingSubID PORTAL_OF_SUMMONING, ESCAPE_TUNNEL, TREASURY, - BANK + BANK, + AURORA_BOREALIS, + DEITY_OF_FIRE }; } diff --git a/lib/constants/StringConstants.h b/lib/constants/StringConstants.h index 97016e852..b3f39f68e 100644 --- a/lib/constants/StringConstants.h +++ b/lib/constants/StringConstants.h @@ -135,7 +135,9 @@ namespace MappedKeys { "library", BuildingSubID::LIBRARY }, { "escapeTunnel", BuildingSubID::ESCAPE_TUNNEL }, { "treasury", BuildingSubID::TREASURY }, - { "bank", BuildingSubID::BANK } + { "bank", BuildingSubID::BANK }, + { "auroraBorealis", BuildingSubID::AURORA_BOREALIS }, + { "deityOfFire", BuildingSubID::DEITY_OF_FIRE } }; static const std::map MARKET_NAMES_TO_TYPES = diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 085674e74..c40242755 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -905,13 +905,6 @@ bool CGTownInstance::hasBuilt(const BuildingID & buildingID) const return vstd::contains(builtBuildings, buildingID); } -bool CGTownInstance::hasBuilt(const BuildingID & buildingID, FactionID townID) const -{ - if (townID == getTown()->faction->getId() || townID == FactionID::ANY) - return hasBuilt(buildingID); - return false; -} - void CGTownInstance::addBuilding(const BuildingID & buildingID) { if(buildingID == BuildingID::NONE) diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 4b1c9811b..b286a762b 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -165,7 +165,6 @@ public: bool hasBuilt(BuildingSubID::EBuildingSubID buildingID) const; //checks if building is constructed and town has same subID bool hasBuilt(const BuildingID & buildingID) const; - bool hasBuilt(const BuildingID & buildingID, FactionID townID) const; void addBuilding(const BuildingID & buildingID); void removeBuilding(const BuildingID & buildingID); void removeAllBuildings(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 7b42be2cc..34a827dd9 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -773,7 +773,7 @@ void CGameHandler::giveSpells(const CGTownInstance *t, const CGHeroInstance *h) ChangeSpells cs; cs.hid = h->id; cs.learn = true; - if (t->hasBuilt(BuildingID::GRAIL, ETownType::CONFLUX) && t->hasBuilt(BuildingID::MAGES_GUILD_1)) + if (t->hasBuilt(BuildingSubID::AURORA_BOREALIS) && t->hasBuilt(BuildingID::MAGES_GUILD_1)) { // Aurora Borealis give spells of all levels even if only level 1 mages guild built for (int i = 0; i < h->maxSpellLevel(); i++) @@ -2115,22 +2115,6 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, } }; - //Performs stuff that has to be done after new building is built - auto processAfterBuiltStructure = [t, this](const BuildingID buildingID) - { - auto isMageGuild = (buildingID <= BuildingID::MAGES_GUILD_5 && buildingID >= BuildingID::MAGES_GUILD_1); - auto isLibrary = isMageGuild ? false - : t->getTown()->buildings.at(buildingID)->subId == BuildingSubID::EBuildingSubID::LIBRARY; - - if(isMageGuild || isLibrary || (t->getFactionID() == ETownType::CONFLUX && buildingID == BuildingID::GRAIL)) - { - if(t->getVisitingHero()) - giveSpells(t,t->getVisitingHero()); - if(t->getGarrisonHero()) - giveSpells(t,t->getGarrisonHero()); - } - }; - //Checks if all requirements will be met with expected building list "buildingsThatWillBe" auto areRequirementsFulfilled = [&buildingsThatWillBe](const BuildingID & buildID) { @@ -2192,8 +2176,20 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, sendAndApply(ns); //Other post-built events. To some logic like giving spells to work gamestate changes for new building must be already in place! - for(auto builtID : ns.bid) - processAfterBuiltStructure(builtID); + for(auto buildingID : ns.bid) + { + bool isMageGuild = buildingID <= BuildingID::MAGES_GUILD_5 && buildingID >= BuildingID::MAGES_GUILD_1; + bool isLibrary = t->getTown()->buildings.at(buildingID)->subId == BuildingSubID::LIBRARY; + bool isAurora = t->getTown()->buildings.at(buildingID)->subId == BuildingSubID::AURORA_BOREALIS; + + if(isMageGuild || isLibrary || isAurora) + { + if(t->getVisitingHero()) + giveSpells(t,t->getVisitingHero()); + if(t->getGarrisonHero()) + giveSpells(t,t->getGarrisonHero()); + } + }; // now when everything is built - reveal tiles for lookout tower changeFogOfWar(t->getSightCenter(), t->getSightRadius(), t->getOwner(), ETileVisibility::REVEALED); diff --git a/server/processors/NewTurnProcessor.cpp b/server/processors/NewTurnProcessor.cpp index 69376f1e3..af343e545 100644 --- a/server/processors/NewTurnProcessor.cpp +++ b/server/processors/NewTurnProcessor.cpp @@ -512,7 +512,7 @@ std::tuple NewTurnProcessor::pickWeekType(bool newMonth) for (const auto & townID : gameHandler->gameState().getMap().getAllTowns()) { const auto * t = gameHandler->gameState().getTown(townID); - if (t->hasBuilt(BuildingID::GRAIL, ETownType::INFERNO)) + if (t->hasBuilt(BuildingSubID::DEITY_OF_FIRE)) return { EWeekType::DEITYOFFIRE, CreatureID::IMP }; } From 2191e51d48c7c85e2e83b93d068eed8599b7191a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 2 Nov 2025 11:09:42 +0200 Subject: [PATCH 05/10] Implemeted HATES_TRAIT bonus, similar to HATE, but targets any unit with specific bonus --- docs/modders/Bonus/Bonus_Types.md | 19 ++++++++++++++++++- lib/battle/DamageCalculator.cpp | 18 ++++++++++++++++-- lib/battle/DamageCalculator.h | 3 ++- lib/bonuses/BonusCustomTypes.cpp | 17 +++++++++++++++++ lib/bonuses/BonusCustomTypes.h | 23 ++++++++++++++++++++++- lib/bonuses/BonusEnum.h | 1 + lib/json/JsonBonus.cpp | 8 ++++++++ 7 files changed, 84 insertions(+), 5 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 66cad30e0..432ed3903 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -543,9 +543,26 @@ When affected unit is attacked from behind, it will receive more damage when att Affected unit will deal more damage when attacking specific creature -- subtype - identifier of hated creature, ie. "creature.genie" +- subtype - identifier of hated creature, ie. `genie` - val - additional damage, percentage +### HATES_TRAIT + +Affected unit will deal more damage when attacking unit that has specific bonus + +- subtype - identifier of hated bonus, ie. `UNDEAD` +- val - additional damage, percentage + +Example: Unit deals 50% more damage to any target that has UNDEAD bonus + +```json + "hatesUndead" : { + "type" : "HATES_TRAIT", + "subtype" : "UNDEAD", + "val" : 50 + } +``` + ### SPELL_LIKE_ATTACK Affected unit ranged attack will use animation and range of specified spell (Magog, Lich) diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 3a53c0424..144d58b9b 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -285,13 +285,26 @@ double DamageCalculator::getAttackFromBackFactor() const return 0; } -double DamageCalculator::getAttackHateFactor() const +double DamageCalculator::getAttackHateCreatureFactor() const { //assume that unit have only few HATE features and cache them all auto allHateEffects = info.attacker->getBonusesOfType(BonusType::HATE); return allHateEffects->valOfBonuses(Selector::subtype()(BonusSubtypeID(info.defender->creatureId()))) / 100.0; } +double DamageCalculator::getAttackHateTraitFactor() const +{ + //assume that unit have only few HATE features and cache them all + auto allHateEffects = info.attacker->getBonusesOfType(BonusType::HATES_TRAIT); + + auto selector = [this](const Bonus* hateBonus) -> bool + { + return info.defender->hasBonusOfType(hateBonus->subtype.as().toEnum()); + }; + + return allHateEffects->valOfBonuses(selector) / 100.0; +} + double DamageCalculator::getAttackRevengeFactor() const { if(info.attacker->hasBonusOfType(BonusType::REVENGE)) //HotA Haspid ability @@ -475,7 +488,8 @@ std::vector DamageCalculator::getAttackFactors() const getAttackFromBackFactor(), getAttackDeathBlowFactor(), getAttackDoubleDamageFactor(), - getAttackHateFactor(), + getAttackHateCreatureFactor(), + getAttackHateTraitFactor(), getAttackRevengeFactor() }; } diff --git a/lib/battle/DamageCalculator.h b/lib/battle/DamageCalculator.h index d0d649f71..c729c0cb6 100644 --- a/lib/battle/DamageCalculator.h +++ b/lib/battle/DamageCalculator.h @@ -50,7 +50,8 @@ class DLL_LINKAGE DamageCalculator double getAttackJoustingFactor() const; double getAttackDeathBlowFactor() const; double getAttackDoubleDamageFactor() const; - double getAttackHateFactor() const; + double getAttackHateCreatureFactor() const; + double getAttackHateTraitFactor() const; double getAttackRevengeFactor() const; double getAttackFromBackFactor() const; diff --git a/lib/bonuses/BonusCustomTypes.cpp b/lib/bonuses/BonusCustomTypes.cpp index 6f6ac83b0..579a97102 100644 --- a/lib/bonuses/BonusCustomTypes.cpp +++ b/lib/bonuses/BonusCustomTypes.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "BonusCustomTypes.h" +#include "CBonusTypeHandler.h" +#include "GameLibrary.h" VCMI_LIB_NAMESPACE_BEGIN @@ -75,4 +77,19 @@ std::string BonusCustomSource::encode(const si32 index) return std::to_string(index); } +std::string BonusTypeID::encode(int32_t index) +{ + if (index == static_cast(BonusType::NONE)) + return ""; + return LIBRARY->bth->bonusToString(static_cast(index)); +} + +si32 BonusTypeID::decode(const std::string & identifier) +{ + if (identifier.empty()) + return RiverId::NO_RIVER.getNum(); + + return resolveIdentifier("bonus", identifier); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusCustomTypes.h b/lib/bonuses/BonusCustomTypes.h index 2520549ad..050717178 100644 --- a/lib/bonuses/BonusCustomTypes.h +++ b/lib/bonuses/BonusCustomTypes.h @@ -11,6 +11,7 @@ #include "../constants/EntityIdentifiers.h" #include "../constants/VariantIdentifier.h" +#include "BonusEnum.h" VCMI_LIB_NAMESPACE_BEGIN @@ -77,7 +78,27 @@ public: static BonusCustomSubtype creatureLevel(int level); }; -using BonusSubtypeID = VariantIdentifier; +class DLL_LINKAGE BonusTypeID : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + using EnumType = BonusType; + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); + + constexpr EnumType toEnum() const + { + return static_cast(EntityIdentifier::num); + } + + constexpr BonusTypeID(const EnumType & enumValue) + { + EntityIdentifier::num = static_cast(enumValue); + } +}; + +using BonusSubtypeID = VariantIdentifier; using BonusSourceID = VariantIdentifier; VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index a3b3e839f..665072907 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -194,6 +194,7 @@ class JsonNode; BONUS_NAME(TRANSMUTATION_IMMUNITY) /*blocks TRANSMUTATION bonus*/\ BONUS_NAME(COMBAT_MANA_BONUS) /* Additional mana per combat */ \ BONUS_NAME(SPECIFIC_SPELL_RANGE) /* value used for allowed spell range, subtype - spell id */\ + BONUS_NAME(HATES_TRAIT) /* affected unit deals additional damage to units with specific bonus. subtype - bonus, val - damage bonus percent */ \ /* end of list */ diff --git a/lib/json/JsonBonus.cpp b/lib/json/JsonBonus.cpp index 872ed3242..3405668a8 100644 --- a/lib/json/JsonBonus.cpp +++ b/lib/json/JsonBonus.cpp @@ -91,6 +91,14 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso }); break; } + case BonusType::HATES_TRAIT: + { + LIBRARY->identifiers()->requestIdentifier( "bonus", node, [&subtype](int32_t identifier) + { + subtype = BonusType(identifier); + }); + break; + } case BonusType::NO_TERRAIN_PENALTY: { LIBRARY->identifiers()->requestIdentifier( "terrain", node, [&subtype](int32_t identifier) From f4c65fe271a37c419dec205728c465c51b39665e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 2 Nov 2025 11:39:12 +0200 Subject: [PATCH 06/10] Show "all spells" icon in Mages Guild when Aurora Borealis is present --- client/windows/CCastleInterface.cpp | 26 +++++++++++++++++++++++++- client/windows/CCastleInterface.h | 12 ++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 60eae5819..3b77a23ee 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -2184,8 +2184,16 @@ void CMageGuildScreen::updateSpells(ObjectInstanceID tID) uint32_t spellCount = town->spellsAtLevel(i+1,false); //spell at level with -1 hmmm? for(uint32_t j=0; jmageGuildLevel() && town->spells[i].size()>j) + if (town->hasBuilt(BuildingSubID::AURORA_BOREALIS)) + { + std::string auroraBorealisName = town->getTown()->getSpecialBuilding(BuildingSubID::AURORA_BOREALIS)->getNameTranslated(); + + auroraBorealisScrolls.push_back(std::make_shared(positions[i][j], auroraBorealisName)); + } + else if(imageGuildLevel() && town->spells[i].size()>j) + { spells.push_back(std::make_shared(positions[i][j], town->spells[i][j].toSpell(), townId)); + } else emptyScrolls.push_back(std::make_shared(AnimationPath::builtin("TPMAGES.DEF"), 1, 0, positions[i][j].x, positions[i][j].y)); } @@ -2194,6 +2202,22 @@ void CMageGuildScreen::updateSpells(ObjectInstanceID tID) redraw(); } +CMageGuildScreen::ScrollAllSpells::ScrollAllSpells(Point position, const std::string & buildingName) +{ + constexpr int auroraBorealisImageIndex = 70; + + OBJECT_CONSTRUCTION; + pos += position; + image = std::make_shared(AnimationPath::builtin("SPELLSCR"), auroraBorealisImageIndex); + pos = image->pos; + + MetaString description; + description.appendTextID("core.genrltxt.714"); + description.replaceRawString(buildingName); + + text = std::make_shared(Rect(Point(), pos.dimensions()), description.toString(), description.toString() ); +} + CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell, ObjectInstanceID townId) : spell(Spell), townId(townId) { diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 3ff010091..ba1e3a7f9 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -37,6 +37,7 @@ class CGarrisonInt; class CComponent; class CComponentBox; class LRClickableArea; +class LRClickableAreaWText; class CTextInputWithConfirm; /// Building "button" @@ -394,10 +395,21 @@ class CMageGuildScreen : public CStatusbarWindow void showPopupWindow(const Point & cursorPosition) override; void hover(bool on) override; }; + + class ScrollAllSpells : public CIntObject + { + std::shared_ptr image; + std::shared_ptr text; + + public: + ScrollAllSpells(Point position, const std::string & buildingName); + }; + std::shared_ptr window; std::shared_ptr exit; std::vector> spells; std::vector> emptyScrolls; + std::vector> auroraBorealisScrolls; std::shared_ptr resdatabar; From e54ff1cbb2530543f47e25e89411285165a56192 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 2 Nov 2025 12:37:43 +0200 Subject: [PATCH 07/10] Restore save compatibility with 1.6 --- lib/bonuses/BonusCustomTypes.h | 2 +- lib/gameState/CGameState.cpp | 3 ++- lib/gameState/CGameStateCampaign.h | 2 -- lib/rewardable/Reward.h | 10 +++++++++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/bonuses/BonusCustomTypes.h b/lib/bonuses/BonusCustomTypes.h index 050717178..7d8458091 100644 --- a/lib/bonuses/BonusCustomTypes.h +++ b/lib/bonuses/BonusCustomTypes.h @@ -99,6 +99,6 @@ public: }; using BonusSubtypeID = VariantIdentifier; -using BonusSourceID = VariantIdentifier; +using BonusSourceID = VariantIdentifier; VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 57b198686..c0718d852 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1626,18 +1626,19 @@ void CGameState::loadGame(CLoadFile & file) logGlobal->info("Loading game state..."); CMapHeader dummyHeader; - StartInfo dummyStartInfo; ActiveModsInSaveList dummyActiveMods; file.load(dummyHeader); if (file.hasFeature(ESerializationVersion::NO_RAW_POINTERS_IN_SERIALIZER)) { + StartInfo dummyStartInfo; file.load(dummyStartInfo); file.load(dummyActiveMods); file.load(*this); } else { + auto dummyStartInfo = std::make_shared(); bool dummyA = false; uint32_t dummyB = 0; uint16_t dummyC = 0; diff --git a/lib/gameState/CGameStateCampaign.h b/lib/gameState/CGameStateCampaign.h index b57769aa4..5f430f0ea 100644 --- a/lib/gameState/CGameStateCampaign.h +++ b/lib/gameState/CGameStateCampaign.h @@ -81,11 +81,9 @@ public: { bool dummyA = false; uint32_t dummyB = 0; - uint16_t dummyC = 0; h & dummyA; h & dummyB; - h & dummyC; } } }; diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 2f9d896ca..1edbd20d5 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -143,12 +143,20 @@ struct DLL_LINKAGE Reward final h & movePoints; h & primary; h & secondary; - h & heroBonuses; if (h.version >= Handler::Version::REWARDABLE_EXTENSIONS) { + h & heroBonuses; h & playerBonuses; h & commanderBonuses; } + else + { + std::vector bonuses; + h & bonuses; + for (const auto & bonus : bonuses) + heroBonuses.push_back(std::make_shared(bonus)); + } + h & grantedArtifacts; if (h.version >= Handler::Version::REWARDABLE_EXTENSIONS) { From 155086d802a734dff6e212625bb4dee10b543c4e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 2 Nov 2025 12:38:06 +0200 Subject: [PATCH 08/10] Fix "accumulate creatures" victory condition to be in line with h3 --- lib/gameState/CGameState.cpp | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index c0718d852..ddfa5a89b 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1242,17 +1242,20 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio case EventCondition::HAVE_CREATURES: { //check if in players armies there is enough creatures - int total = 0; //creature counter - for(auto ai : map->getObjects()) - { - if(ai->getOwner() == player) - { - for(const auto & elem : ai->Slots()) //iterate through army - if(elem.second->getId() == condition.objectType.as()) //it's searched creature - total += elem.second->getCount(); - } - } - return total >= condition.value; + // NOTE: only heroes & towns are checked, in line with H3. + // Garrisons, mines, and guards of owned dwellings(!) are excluded + int totalCreatures = 0; + for (const auto & hero : p->getHeroes()) + for(const auto & elem : hero->Slots()) //iterate through army + if(elem.second->getId() == condition.objectType.as()) //it's searched creature + totalCreatures += elem.second->getCount(); + + for (const auto & town : p->getTowns()) + for(const auto & elem : town->Slots()) //iterate through army + if(elem.second->getId() == condition.objectType.as()) //it's searched creature + totalCreatures += elem.second->getCount(); + + return totalCreatures >= condition.value; } case EventCondition::HAVE_RESOURCES: { From bdafb47655a0421bde3aa5c45103336d997140fa Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 2 Nov 2025 12:50:27 +0200 Subject: [PATCH 09/10] Do not activate Life Drain if unit is fully healed --- server/battles/BattleActionProcessor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index cf9340efb..14b8e3e78 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1448,7 +1448,7 @@ void BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & battl } //life drain handling - if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving()) + if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving() && attackerState->getTotalHealth() != attackerState->getAvailableHealth()) { int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(BonusType::LIFE_DRAIN) / 100; healInfo += attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); From 23fb9e88298e49492e986f27694f55261bcf772c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 2 Nov 2025 13:06:24 +0200 Subject: [PATCH 10/10] Update docs & schemas --- config/schemas/townBuilding.json | 2 +- docs/modders/Bonus/Bonus_Types.md | 2 +- docs/modders/Entities_Format/Town_Building_Format.md | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config/schemas/townBuilding.json b/config/schemas/townBuilding.json index 3b4417549..581d37708 100644 --- a/config/schemas/townBuilding.json +++ b/config/schemas/townBuilding.json @@ -36,7 +36,7 @@ }, "type" : { "type" : "string", - "enum" : [ "mysticPond", "castleGate", "portalOfSummoning", "library", "escapeTunnel", "treasury", "bank" ], + "enum" : [ "mysticPond", "castleGate", "portalOfSummoning", "library", "escapeTunnel", "treasury", "bank", "auroraBorealis", "deityOfFire" ], "description" : "Subtype for some special buildings" }, "mode" : { diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 432ed3903..c7b63f8e1 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -548,7 +548,7 @@ Affected unit will deal more damage when attacking specific creature ### HATES_TRAIT -Affected unit will deal more damage when attacking unit that has specific bonus +Affected unit will deal more damage when attacking unit that has specific bonus. Note that this bonus has no assigned description. To make it visible in creature window UI, make sure to provide custom description for such bonus. - subtype - identifier of hated bonus, ie. `UNDEAD` - val - additional damage, percentage diff --git a/docs/modders/Entities_Format/Town_Building_Format.md b/docs/modders/Entities_Format/Town_Building_Format.md index ecc4f7678..6b0c947a7 100644 --- a/docs/modders/Entities_Format/Town_Building_Format.md +++ b/docs/modders/Entities_Format/Town_Building_Format.md @@ -238,15 +238,13 @@ Building requirements can be described using logical expressions: Following Heroes III buildings can be used as unique buildings for a town. Their functionality should be identical to a corresponding H3 building. H3 buildings that are not present in this list contain no hardcoded functionality. See vcmi json configuration to see how such buildings can be implemented in a mod. - `mysticPond` -- `artifactMerchant` -- `freelancersGuild` -- `magicUniversity` - `castleGate` -- `creatureTransformer` - `portalOfSummoning` - `library` - `escapeTunnel` - `treasury` +- `auroraBorealis` +- `deityOfFire` #### Buildings from other Heroes III mods