diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index ac23c2149..b65458358 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -788,14 +788,29 @@ bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj) { if(const CStackInstance * s = obj->getStackPtr(SlotID(i))) { - UpgradeInfo ui; - myCb->fillUpgradeInfo(obj, SlotID(i), ui); - if(ui.oldID != CreatureID::NONE && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count)) + UpgradeInfo ui(s->getId()); + do { - myCb->upgradeCreature(obj, SlotID(i), ui.newID[0]); - upgraded = true; - logAi->debug("Upgraded %d %s to %s", s->count, ui.oldID.toCreature()->getNamePluralTranslated(), ui.newID[0].toCreature()->getNamePluralTranslated()); + myCb->fillUpgradeInfo(obj, SlotID(i), ui); + + if(ui.hasUpgrades()) + { + // creature at given slot might have alternative upgrades, pick best one + CreatureID upgID = *vstd::maxElementByFun(ui.getAvailableUpgrades(), [](const CreatureID & id) + { + return id.toCreature()->getAIValue(); + }); + if(nullkiller->getFreeResources().canAfford(ui.getUpgradeCostsFor(upgID) * s->count)) + { + myCb->upgradeCreature(obj, SlotID(i), upgID); + upgraded = true; + logAi->debug("Upgraded %d %s to %s", s->count, ui.oldID.toCreature()->getNamePluralTranslated(), ui.getNextUpgrade().toCreature()->getNamePluralTranslated()); + } + else + break; + } } + while(ui.hasUpgrades()); } } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 8af203906..473c42b83 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -754,12 +754,28 @@ void makePossibleUpgrades(const CArmedInstance * obj) { if(const CStackInstance * s = obj->getStackPtr(SlotID(i))) { - UpgradeInfo ui; - cb->fillUpgradeInfo(obj, SlotID(i), ui); - if(ui.oldID != CreatureID::NONE && cb->getResourceAmount().canAfford(ui.cost[0] * s->count)) + UpgradeInfo ui(s->getId()); + do { - cb->upgradeCreature(obj, SlotID(i), ui.newID[0]); + cb->fillUpgradeInfo(obj, SlotID(i), ui); + + if(ui.hasUpgrades()) + { + // creature at given slot might have alternative upgrades, pick best one + CreatureID upgID = *vstd::maxElementByFun(ui.getAvailableUpgrades(), [](const CreatureID & id) + { + return id.toCreature()->getAIValue(); + }); + if(cb->getResourceAmount().canAfford(ui.getUpgradeCostsFor(upgID) * s->count)) + { + cb->upgradeCreature(obj, SlotID(i), upgID); + logAi->debug("Upgraded %d %s to %s", s->count, ui.oldID.toCreature()->getNamePluralTranslated(), ui.getNextUpgrade().toCreature()->getNamePluralTranslated()); + } + else + break; + } } + while(ui.hasUpgrades()); } } } diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index af32faf6e..ab86dd194 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -24,7 +24,7 @@ class CCreature; struct CGPath; class CCreatureSet; class CGObjectInstance; -struct UpgradeInfo; +class UpgradeInfo; class ConditionalWait; struct CPathsInfo; diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 78a809e72..67a199d5e 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -162,10 +162,10 @@ std::function CGarrisonSlot::getDismiss() const /// @return Whether the view should be refreshed bool CGarrisonSlot::viewInfo() { - UpgradeInfo pom; + UpgradeInfo pom(ID.getNum()); LOCPLINT->cb->fillUpgradeInfo(getObj(), ID, pom); - bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.oldID != CreatureID::NONE; //upgrade is possible + bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.canUpgrade(); //upgrade is possible std::function upgr = nullptr; auto dism = getDismiss(); if(canUpgrade) upgr = [=] (CreatureID newID) { LOCPLINT->cb->upgradeCreature(getObj(), ID, newID); }; @@ -177,7 +177,7 @@ bool CGarrisonSlot::viewInfo() elem->block(true); redraw(); - GH.windows().createAndPushWindow(myStack, dism, pom, upgr); + GH.windows().createAndPushWindow(myStack, dism, std::move(pom), upgr); return true; } diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 1ef1d08bb..3280292a7 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -57,6 +57,10 @@ public: }; struct StackUpgradeInfo { + StackUpgradeInfo() = delete; + StackUpgradeInfo(UpgradeInfo && upgradeInfo) + : info(std::move(upgradeInfo)) + { } UpgradeInfo info; std::function callback; }; @@ -355,15 +359,15 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) // besides - should commander really be upgradeable? auto & upgradeInfo = parent->info->upgradeInfo.value(); - const size_t buttonsToCreate = std::min(upgradeInfo.info.newID.size(), upgrade.size()); + const size_t buttonsToCreate = std::min(upgradeInfo.info.size(), upgrade.size()); for(size_t buttonIndex = 0; buttonIndex < buttonsToCreate; buttonIndex++) { - TResources totalCost = upgradeInfo.info.cost[buttonIndex] * parent->info->creatureCount; + TResources totalCost = upgradeInfo.info.getUpgradeCosts().at(buttonIndex) * parent->info->creatureCount; auto onUpgrade = [=]() { - upgradeInfo.callback(upgradeInfo.info.newID[buttonIndex]); + upgradeInfo.callback(upgradeInfo.info.getAvailableUpgrades().at(buttonIndex)); parent->close(); }; auto onClick = [=]() @@ -385,7 +389,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) }; auto upgradeBtn = std::make_shared(Point(221 + (int)buttonIndex * 40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CGI->generaltexth->zelp[446], onClick); - upgradeBtn->setOverlay(std::make_shared(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); + upgradeBtn->setOverlay(std::make_shared(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.getAvailableUpgrades()[buttonIndex]]->getIconIndex())); if(buttonsToCreate == 1) // single upgrade available upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE; @@ -755,7 +759,7 @@ CStackWindow::CStackWindow(const CStackInstance * stack, bool popup) init(); } -CStackWindow::CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & upgradeInfo, std::function callback) +CStackWindow::CStackWindow(const CStackInstance * stack, std::function dismiss, UpgradeInfo && upgradeInfo, std::function callback) : CWindowObject(BORDERED), info(new UnitView()) { @@ -763,9 +767,8 @@ CStackWindow::CStackWindow(const CStackInstance * stack, std::function d info->creature = stack->getCreature(); info->creatureCount = stack->count; - info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo()); + info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo(std::move(upgradeInfo))); info->dismissInfo = std::make_optional(UnitView::StackDismissInfo()); - info->upgradeInfo->info = upgradeInfo; info->upgradeInfo->callback = callback; info->dismissInfo->callback = dismiss; info->owner = dynamic_cast (stack->armyObj); diff --git a/client/windows/CCreatureWindow.h b/client/windows/CCreatureWindow.h index acf43029a..60ac9f315 100644 --- a/client/windows/CCreatureWindow.h +++ b/client/windows/CCreatureWindow.h @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CCommanderInstance; class CStackInstance; class CStack; -struct UpgradeInfo; +class UpgradeInfo; VCMI_LIB_NAMESPACE_END @@ -204,7 +204,7 @@ public: // for normal stacks in armies CStackWindow(const CStackInstance * stack, bool popup); - CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & info, std::function callback); + CStackWindow(const CStackInstance * stack, std::function dismiss, UpgradeInfo && info, std::function callback); // for commanders & commander level-up dialog CStackWindow(const CCommanderInstance * commander, bool popup); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index bd9cec14f..828c4df70 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1153,12 +1153,15 @@ void CHillFortWindow::updateGarrisons() State newState = getState(SlotID(i)); if(newState != State::EMPTY) { - UpgradeInfo info; - LOCPLINT->cb->fillUpgradeInfo(hero, SlotID(i), info); - if(info.newID.size())//we have upgrades here - update costs + if(const CStackInstance * s = hero->getStackPtr(SlotID(i))) { - costs[i] = info.cost.back() * hero->getStackCount(SlotID(i)); - totalSum += costs[i]; + UpgradeInfo info(s->getCreature()->getId()); + LOCPLINT->cb->fillUpgradeInfo(hero, SlotID(i), info); + if(info.canUpgrade()) //we have upgrades here - update costs + { + costs[i] = info.getNextUpgradeCosts() * hero->getStackCount(SlotID(i)); + totalSum += costs[i]; + } } } @@ -1264,9 +1267,12 @@ void CHillFortWindow::makeDeal(SlotID slot) { if(slot.getNum() == i || ( slot.getNum() == slotsCount && currState[i] == State::MAKE_UPGRADE ))//this is activated slot or "upgrade all" { - UpgradeInfo info; - LOCPLINT->cb->fillUpgradeInfo(hero, SlotID(i), info); - LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.newID.back()); + if(const CStackInstance * s = hero->getStackPtr(SlotID(i))) + { + UpgradeInfo info(s->getCreatureID()); + LOCPLINT->cb->fillUpgradeInfo(hero, SlotID(i), info); + LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.getNextUpgrade()); + } } } break; @@ -1295,18 +1301,15 @@ CHillFortWindow::State CHillFortWindow::getState(SlotID slot) if(hero->slotEmpty(slot)) return State::EMPTY; - UpgradeInfo info; + UpgradeInfo info(hero->getStackPtr(slot)->getCreatureID()); LOCPLINT->cb->fillUpgradeInfo(hero, slot, info); - if (info.newID.empty()) - { - // Hill Fort may limit level of upgradeable creatures, e.g. mini Hill Fort from HOTA - if (hero->getCreature(slot)->hasUpgrades()) - return State::UNAVAILABLE; + if(info.hasUpgrades() && !info.canUpgrade()) + return State::UNAVAILABLE; // Hill Fort may limit level of upgradeable creatures, e.g. mini Hill Fort from HOTA + if(!info.hasUpgrades()) return State::ALREADY_UPGRADED; - } - if(!(info.cost.back() * hero->getStackCount(slot)).canBeAfforded(myRes)) + if(!(info.getNextUpgradeCosts() * hero->getStackCount(slot)).canBeAfforded(myRes)) return State::UNAFFORDABLE; return State::MAKE_UPGRADE; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index b426b5418..1b642c25f 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -183,7 +183,7 @@ const IMarket * CGameInfoCallback::getMarket(ObjectInstanceID objid) const return nullptr; } -void CGameInfoCallback::fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const +void CGameInfoCallback::fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo & out) const { //boost::shared_lock lock(*gs->mx); ERROR_RET_IF(!canGetFullInfo(obj), "Cannot get info about not owned object!"); diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 25f15d53e..a4df86d3f 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -33,7 +33,7 @@ struct CPathsInfo; struct InfoAboutHero; struct InfoAboutTown; -struct UpgradeInfo; +class UpgradeInfo; struct SThievesGuildInfo; class CMapHeader; struct TeamState; @@ -172,7 +172,7 @@ public: //armed object - virtual void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out)const; + virtual void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const; //hero const CGHeroInstance * getHero(ObjectInstanceID objid) const override; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 4b8f99d58..9249bd4a2 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1088,10 +1088,11 @@ void CGameState::fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, Upg out = fillUpgradeInfo(obj->getStack(stackPos)); } -UpgradeInfo CGameState::fillUpgradeInfo(const CStackInstance &stack) const +UpgradeInfo CGameState::fillUpgradeInfo(const CStackInstance & stack) const { - UpgradeInfo ret; const CCreature *base = stack.getCreature(); + + UpgradeInfo ret(base->getId()); if (stack.armyObj->ID == Obj::HERO) { @@ -1117,12 +1118,6 @@ UpgradeInfo CGameState::fillUpgradeInfo(const CStackInstance &stack) const town->fillUpgradeInfo(ret, stack); } - if(!ret.newID.empty()) - ret.oldID = base->getId(); - - for (ResourceSet &cost : ret.cost) - cost.positive(); //upgrade cost can't be negative, ignore missing resources - return ret; } @@ -1784,4 +1779,22 @@ ArtifactID CGameState::pickRandomArtifact(vstd::RNG & rand, int flags) return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); } +void UpgradeInfo::addUpgrade(const CreatureID & upgradeID, ResourceSet && upgradeCost, bool available) +{ + isAvailable = available; + upgradesIDs.push_back(upgradeID); + + upgradeCost.positive(); //upgrade cost can't be negative, ignore missing resources + upgradesCosts.push_back(std::move(upgradeCost)); + + // sort from highest ID to smallest + size_t pos = upgradesIDs.size() - 1; + while(pos > 0 && upgradesIDs[pos] > upgradesIDs[pos - 1]) + { + std::swap(upgradesIDs[pos], upgradesIDs[pos - 1]); + std::swap(upgradesCosts[pos], upgradesCosts[pos - 1]); + --pos; + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index a149575b4..66117bade 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -39,12 +39,67 @@ struct SThievesGuildInfo; class CRandomGenerator; class GameSettings; -struct UpgradeInfo +class UpgradeInfo { +public: + UpgradeInfo() = delete; + UpgradeInfo(CreatureID base) + : oldID(base), isAvailable(true) + { } + CreatureID oldID; //creature to be upgraded - std::vector newID; //possible upgrades - std::vector cost; // cost[upgrade_serial] -> set of pairs; cost is for single unit (not entire stack) - UpgradeInfo(){oldID = CreatureID::NONE;}; + + const auto & getAvailableUpgrades() const + { + return upgradesIDs; + } + + const CreatureID & getNextUpgrade() const + { + return upgradesIDs.back(); + } + + const auto & getUpgradeCostsFor(CreatureID id) const + { + auto idIt = std::find(upgradesIDs.begin(), upgradesIDs.end(), id); + + assert(idIt != upgradesIDs.end()); + + return upgradesCosts[std::distance(upgradesIDs.begin(), idIt)]; + } + + const auto & getUpgradeCosts() const + { + return upgradesCosts; + } + + const auto & getNextUpgradeCosts() const + { + return upgradesCosts.back(); + } + + bool canUpgrade() const + { + return !upgradesIDs.empty() && isAvailable; + } + + bool hasUpgrades() const + { + return !upgradesIDs.empty(); + } + + // Adds a new upgrade and ensures alignment and sorted order + void addUpgrade(const CreatureID & upgradeID, ResourceSet && upgradeCost, bool available = true); + + auto size() const + { + return upgradesIDs.size(); + } + +private: + std::vector upgradesIDs; //possible upgrades + std::vector upgradesCosts; // cost[upgrade_serial] -> set of pairs; cost is for single unit (not entire stack) + bool isAvailable; // flag for unavailableUpgrades like in miniHillFort from HoTA }; class BattleInfo; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index acc563274..c43ee2a60 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1855,8 +1855,7 @@ void CGHeroInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &s auto nid = CreatureID(it->additionalInfo[0]); if (nid != stack.getId()) //in very specific case the upgrade is available by default (?) { - info.newID.push_back(nid); - info.cost.push_back(nid.toCreature()->getFullRecruitCost() - stack.getType()->getFullRecruitCost()); + info.addUpgrade(std::move(nid), nid.toCreature()->getFullRecruitCost() - stack.getType()->getFullRecruitCost()); } } } diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 037e98d22..011001bc5 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1234,8 +1234,7 @@ void CGTownInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &s { if(vstd::contains(stack.getCreature()->upgrades, upgrID)) //possible upgrade { - info.newID.push_back(upgrID); - info.cost.push_back(upgrID.toCreature()->getFullRecruitCost() - stack.getType()->getFullRecruitCost()); + info.addUpgrade(upgrID, upgrID.toCreature()->getFullRecruitCost() - stack.getType()->getFullRecruitCost()); } } } diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index 4398c5a11..b15859048 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -23,7 +23,7 @@ class RNG; } struct BattleResult; -struct UpgradeInfo; +class UpgradeInfo; class BoatId; class CGObjectInstance; class CStackInstance; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 910baa639..2bf287a32 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -1323,13 +1323,12 @@ void HillFort::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) int costModifier = upgradeCostPercentage[index]; - if (costModifier < 0) - return; // upgrade not allowed + if(costModifier < 0) + return; for(const auto & nid : stack.getCreature()->upgrades) { - info.newID.push_back(nid); - info.cost.push_back((nid.toCreature()->getFullRecruitCost() - stack.getType()->getFullRecruitCost()) * costModifier / 100); + info.addUpgrade(nid, (nid, (nid.toCreature()->getFullRecruitCost() - stack.getType()->getFullRecruitCost()) * costModifier / 100), costModifier >= 0); } } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index e73ef1e88..7063ebdba 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2414,19 +2414,19 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI { COMPLAIN_RET("Cannot upgrade, no stack at slot " + std::to_string(pos)); } - UpgradeInfo ui; + UpgradeInfo ui(obj->getStackPtr(pos)->getId()); fillUpgradeInfo(obj, pos, ui); PlayerColor player = obj->tempOwner; const PlayerState *p = getPlayerState(player); int crQuantity = obj->stacks.at(pos)->count; - int newIDpos= vstd::find_pos(ui.newID, upgID);//get position of new id in UpgradeInfo + int newIDpos= vstd::find_pos(ui.getAvailableUpgrades(), upgID);//get position of new id in UpgradeInfo //check if upgrade is possible - if ((ui.oldID == CreatureID::NONE || newIDpos == -1) && complain("That upgrade is not possible!")) + if (!ui.hasUpgrades() && complain("That upgrade is not possible!")) { return false; } - TResources totalCost = ui.cost.at(newIDpos) * crQuantity; + TResources totalCost = ui.getUpgradeCostsFor(upgID) * crQuantity; //check if player has enough resources if (!p->resources.canAfford(totalCost))