1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-23 22:37:55 +02:00

Unit stack rebalancing rework

- CStackInstance::count is now private with accessor methods
- CStackInstance::experience renamed to totalExperience and now stores
total stack experience (multiplied by stack size) to reduce rounding
errors
- CStackInstance::totalExperience is now private with accessors methods
- stack experience is now automatically reallocated on stack management
- Removed buggy BulkSmartRebalanceStacks pack, that mostly duplicates
BulkRebalanceStacks
- Renamed BulkSmartSplitStack to BulkSplitAndRebalanceStack to drop
unclear "smart" in name
- Reworked split-and-rebalance logic to correctly reallocate stack
experience
This commit is contained in:
Ivan Savenko
2025-05-01 13:41:48 +03:00
parent ac26b3ed9b
commit 283adc37d7
52 changed files with 339 additions and 328 deletions

View File

@@ -125,7 +125,7 @@ std::vector<SlotID> CCreatureSet::getCreatureSlots(const CCreature * c, const Sl
if(!elem.second || !elem.second->getType() || elem.second->getType() != c)
continue;
if(elem.second->count == ignoreAmount || elem.second->count < 1)
if(elem.second->getCount() == ignoreAmount || elem.second->getCount() < 1)
continue;
result.push_back(elem.first);
@@ -144,7 +144,7 @@ bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmoun
if(!elem.second || !elem.second->getType() || elem.second->getType() != c)
continue;
const auto count = elem.second->count;
const auto count = elem.second->getCount();
if(count == ignoreAmount || count < 1)
continue;
@@ -236,20 +236,19 @@ TCreatureQueue CCreatureSet::getCreatureQueue(const SlotID & exclude) const
TQuantity CCreatureSet::getStackCount(const SlotID & slot) const
{
auto i = stacks.find(slot);
if (i != stacks.end())
return i->second->count;
else
return 0; //TODO? consider issuing a warning
if (!hasStackAtSlot(slot))
return 0;
return stacks.at(slot)->getCount();
}
TExpType CCreatureSet::getStackExperience(const SlotID & slot) const
TExpType CCreatureSet::getStackTotalExperience(const SlotID & slot) const
{
auto i = stacks.find(slot);
if (i != stacks.end())
return i->second->experience;
else
return 0; //TODO? consider issuing a warning
return stacks.at(slot)->getTotalExperience();
}
TExpType CCreatureSet::getStackAverageExperience(const SlotID & slot) const
{
return stacks.at(slot)->getAverageExperience();
}
bool CCreatureSet::mergeableStacks(std::pair<SlotID, SlotID> & out, const SlotID & preferable) const /*looks for two same stacks, returns slot positions */
@@ -284,19 +283,6 @@ bool CCreatureSet::mergeableStacks(std::pair<SlotID, SlotID> & out, const SlotID
return false;
}
void CCreatureSet::sweep()
{
for(auto i=stacks.begin(); i!=stacks.end(); ++i)
{
if(!i->second->count)
{
stacks.erase(i);
sweep();
break;
}
}
}
void CCreatureSet::addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging)
{
const CCreature *c = cre.toCreature();
@@ -437,23 +423,24 @@ void CCreatureSet::setFormation(EArmyFormation mode)
void CCreatureSet::setStackCount(const SlotID & slot, TQuantity count)
{
assert(hasStackAtSlot(slot));
assert(stacks[slot]->count + count > 0);
if (count > stacks[slot]->count)
stacks[slot]->experience = static_cast<TExpType>(stacks[slot]->experience * (count / static_cast<double>(stacks[slot]->count)));
stacks[slot]->count = count;
stacks.at(slot)->setCount(count);
armyChanged();
}
void CCreatureSet::giveStackExp(TExpType exp)
void CCreatureSet::giveAverageStackExperience(TExpType exp)
{
for(TSlots::const_iterator i = stacks.begin(); i != stacks.end(); i++)
i->second->giveStackExp(exp);
for(const auto & stack : stacks)
{
stack.second->giveAverageStackExperience(exp);
stack.second->nodeHasChanged();
}
}
void CCreatureSet::setStackExp(const SlotID & slot, TExpType exp)
void CCreatureSet::giveTotalStackExperience(const SlotID & slot, TExpType exp)
{
assert(hasStackAtSlot(slot));
stacks[slot]->experience = exp;
stacks[slot]->giveTotalStackExperience(exp);
stacks[slot]->nodeHasChanged();
}
void CCreatureSet::clearSlots()
@@ -528,7 +515,23 @@ void CCreatureSet::joinStack(const SlotID & slot, std::unique_ptr<CStackInstance
assert(c);
//TODO move stuff
changeStackCount(slot, stack->count);
changeStackCount(slot, stack->getCount());
giveTotalStackExperience(slot, stack->getTotalExperience());
}
std::unique_ptr<CStackInstance> CCreatureSet::splitStack(const SlotID & slot, TQuantity toSplit)
{
auto & currentStack = stacks.at(slot);
assert(currentStack->getCount() > toSplit);
TExpType experienceBefore = currentStack->getTotalExperience();
currentStack->setCount(currentStack->getCount() - toSplit);
TExpType experienceAfter = currentStack->getTotalExperience();
auto newStack = std::make_unique<CStackInstance>(currentStack->cb, currentStack->getCreatureID(), toSplit);
newStack->giveTotalStackExperience(experienceBefore - experienceAfter);
return newStack;
}
void CCreatureSet::changeStackCount(const SlotID & slot, TQuantity toAdd)
@@ -674,14 +677,13 @@ void CCreatureSet::serializeJson(JsonSerializeFormat & handler, const std::strin
CStackInstance::CStackInstance(IGameCallback *cb, bool isHypothetic)
: CBonusSystemNode(isHypothetic)
, CStackBasicDescriptor(nullptr, 0)
, CArtifactSet(cb)
, GameCallbackHolder(cb)
, nativeTerrain(this, Selector::type()(BonusType::TERRAIN_NATIVE))
, initiative(this, Selector::type()(BonusType::STACKS_SPEED))
, totalExperience(0)
{
experience = 0;
count = 0;
setType(nullptr);
setNodeType(STACK_INSTANCE);
}
@@ -689,12 +691,12 @@ CStackInstance::CStackInstance(IGameCallback *cb, const CreatureID & id, TQuanti
: CStackInstance(cb, false)
{
setType(id);
count = Count;
setCount(Count);
}
CCreature::CreatureQuantityId CStackInstance::getQuantityID() const
{
return CCreature::getQuantityID(count);
return CCreature::getQuantityID(getCount());
}
int CStackInstance::getExpRank() const
@@ -706,7 +708,7 @@ int CStackInstance::getExpRank() const
{
for(int i = static_cast<int>(LIBRARY->creh->expRanks[tier].size()) - 2; i > -1; --i) //sic!
{ //exp values vary from 1st level to max exp at 11th level
if (experience >= LIBRARY->creh->expRanks[tier][i])
if (getAverageExperience() >= LIBRARY->creh->expRanks[tier][i])
return ++i; //faster, but confusing - 0 index mean 1st level of experience
}
return 0;
@@ -715,7 +717,7 @@ int CStackInstance::getExpRank() const
{
for(int i = static_cast<int>(LIBRARY->creh->expRanks[0].size()) - 2; i > -1; --i)
{
if (experience >= LIBRARY->creh->expRanks[0][i])
if (getAverageExperience() >= LIBRARY->creh->expRanks[0][i])
return ++i;
}
return 0;
@@ -727,17 +729,47 @@ int CStackInstance::getLevel() const
return std::max(1, getType()->getLevel());
}
void CStackInstance::giveStackExp(TExpType exp)
void CStackInstance::giveAverageStackExperience(TExpType desiredAmountPerUnit)
{
int level = getType()->getLevel();
if (!vstd::iswithin(level, 1, 7))
level = 0;
if (!canGainExperience())
return;
ui32 maxExp = LIBRARY->creh->expRanks[level].back();
int level = std::clamp(getLevel(), 1, 7);
TExpType maxAmountPerUnit = LIBRARY->creh->expRanks[level].back();
TExpType actualAmountPerUnit = std::min(desiredAmountPerUnit, maxAmountPerUnit * LIBRARY->creh->maxExpPerBattle[level]/100);
TExpType maxExperience = maxAmountPerUnit * getCount();
TExpType maxExperienceToGain = maxExperience - totalExperience;
TExpType actualGainedExperience = std::min(maxExperienceToGain, actualAmountPerUnit * getCount());
vstd::amin(exp, static_cast<TExpType>(maxExp)); //prevent exp overflow due to different types
vstd::amin(exp, (maxExp * LIBRARY->creh->maxExpPerBattle[level])/100);
vstd::amin(experience += exp, maxExp); //can't get more exp than this limit
totalExperience += actualGainedExperience;
}
void CStackInstance::giveTotalStackExperience(TExpType experienceToGive)
{
if (!canGainExperience())
return;
int level = std::clamp(getLevel(), 1, 7);
TExpType maxAmountPerUnit = LIBRARY->creh->expRanks[level].back();
TExpType maxExperience = maxAmountPerUnit * getCount();
TExpType maxExperienceToGain = maxExperience - totalExperience;
TExpType actualGainedExperience = std::min(maxExperienceToGain, experienceToGive);
totalExperience += actualGainedExperience;
}
TExpType CStackInstance::getTotalExperience() const
{
return totalExperience;
}
TExpType CStackInstance::getAverageExperience() const
{
return totalExperience / getCount();
}
bool CStackInstance::canGainExperience() const
{
return cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE);
}
void CStackInstance::setType(const CreatureID & creID)
@@ -753,8 +785,8 @@ void CStackInstance::setType(const CCreature *c)
if(getCreature())
{
detachFromSource(*getCreature());
if (getCreature()->isMyUpgrade(c) && LIBRARY->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE))
experience = static_cast<TExpType>(experience * LIBRARY->creh->expAfterUpgrade / 100.0);
if (LIBRARY->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE))
totalExperience = totalExperience * LIBRARY->creh->expAfterUpgrade / 100;
}
CStackBasicDescriptor::setType(c);
@@ -762,6 +794,20 @@ void CStackInstance::setType(const CCreature *c)
if(getCreature())
attachToSource(*getCreature());
}
void CStackInstance::setCount(TQuantity newCount)
{
assert(newCount >= 0);
if (newCount < getCount())
{
TExpType averageExperience = totalExperience / getCount();
totalExperience = averageExperience * newCount;
}
CStackBasicDescriptor::setCount(newCount);
}
std::string CStackInstance::bonusToString(const std::shared_ptr<Bonus>& bonus, bool description) const
{
return LIBRARY->getBth()->bonusToString(bonus, this, description);
@@ -831,7 +877,7 @@ bool CStackInstance::valid(bool allowUnrandomized) const
std::string CStackInstance::nodeName() const
{
std::ostringstream oss;
oss << "Stack of " << count << " of ";
oss << "Stack of " << getCount() << " of ";
if(getType())
oss << getType()->getNamePluralTextID();
else
@@ -877,19 +923,19 @@ CreatureID CStackInstance::getCreatureID() const
std::string CStackInstance::getName() const
{
return (count > 1) ? getType()->getNamePluralTranslated() : getType()->getNameSingularTranslated();
return (getCount() > 1) ? getType()->getNamePluralTranslated() : getType()->getNameSingularTranslated();
}
ui64 CStackInstance::getPower() const
{
assert(getType());
return static_cast<ui64>(getType()->getAIValue()) * count;
return static_cast<ui64>(getType()->getAIValue()) * getCount();
}
ui64 CStackInstance::getMarketValue() const
{
assert(getType());
return getType()->getFullRecruitCost().marketValue() * count;
return getType()->getFullRecruitCost().marketValue() * getCount();
}
ArtBearer::ArtBearer CStackInstance::bearerType() const
@@ -968,9 +1014,8 @@ CCommanderInstance::CCommanderInstance(IGameCallback *cb, const CreatureID & id)
, name("Commando")
{
alive = true;
experience = 0;
level = 1;
count = 1;
setCount(1);
setType(nullptr);
setNodeType (CBonusSystemNode::COMMANDER);
secondarySkills.resize (ECommander::SPELL_POWER + 1);
@@ -988,15 +1033,14 @@ void CCommanderInstance::setAlive (bool Alive)
}
}
void CCommanderInstance::giveStackExp (TExpType exp)
bool CCommanderInstance::canGainExperience() const
{
if (alive)
experience += exp;
return alive && CStackInstance::canGainExperience();
}
int CCommanderInstance::getExpRank() const
{
return LIBRARY->heroh->level (experience);
return LIBRARY->heroh->level (getTotalExperience());
}
int CCommanderInstance::getLevel() const
@@ -1020,7 +1064,7 @@ ArtBearer::ArtBearer CCommanderInstance::bearerType() const
bool CCommanderInstance::gainsLevel() const
{
return experience >= LIBRARY->heroh->reqExp(level + 1);
return getTotalExperience() >= LIBRARY->heroh->reqExp(level + 1);
}
//This constructor should be placed here to avoid side effects
@@ -1062,6 +1106,12 @@ void CStackBasicDescriptor::setType(const CCreature * c)
typeID = c ? c->getId() : CreatureID();
}
void CStackBasicDescriptor::setCount(TQuantity newCount)
{
assert(newCount >= 0);
count = newCount;
}
bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r)
{
return l.typeID == r.typeID && l.count == r.count;