diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 4eaa690be..dbd5060e1 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -271,7 +271,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) { InfoAboutTown iah; - LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentTown()); //todo: should this be nearest hero? + LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero? OBJECT_CONSTRUCTION; tooltip = std::make_shared(Point(9, 10), iah); @@ -281,7 +281,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("HEROQVBK"), toScreen(position)) { InfoAboutHero iah; - LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentHero()); //todo: should this be nearest hero? + LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero? OBJECT_CONSTRUCTION; tooltip = std::make_shared(Point(9, 10), iah); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index e2c88acb0..87a93fa22 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -225,6 +225,9 @@ TResources CGTownInstance::dailyIncome() const } } + if (!getOwner().isValidPlayer()) + return ret; + const auto & playerSettings = cb->getPlayerSettings(getOwner()); ret.applyHandicap(playerSettings->handicap.percentIncome); return ret; @@ -473,67 +476,50 @@ void CGTownInstance::initObj(vstd::RNG & rand) ///initialize town structures } } initializeConfigurableBuildings(rand); + initializeNeutralTownGarrison(rand); recreateBuildingsBonuses(); updateAppearance(); } +void CGTownInstance::initializeNeutralTownGarrison(vstd::RNG & rand) +{ + struct RandomGuardsInfo{ + int tier; + int chance; + int min; + int max; + }; + + constexpr std::array randomGuards = { + RandomGuardsInfo{ 0, 33, 8, 15 }, + RandomGuardsInfo{ 1, 33, 5, 7 }, + RandomGuardsInfo{ 2, 20, 3, 5 }, + RandomGuardsInfo{ 3, 14, 1, 3 }, + }; + + // Only neutral towns may get initial garrison + if (getOwner().isValidPlayer()) + return; + + // Only towns with garrison not set in map editor may get initial garrison + // FIXME: H3 editor allow explicitly empty garrison, but vcmi loses this flag on load + if (stacksCount() > 0) + return; + + for (auto const & guard : randomGuards) + { + if (rand.nextInt(99) >= guard.chance) + continue; + + CreatureID guardID = getTown()->creatures[guard.tier].at(0); + int guardSize = rand.nextInt(guard.min, guard.max); + + putStack(getFreeSlot(), new CStackInstance(guardID, guardSize)); + } +} + void CGTownInstance::newTurn(vstd::RNG & rand) const { - if (cb->getDate(Date::DAY_OF_WEEK) == 1) //reset on new week - { - if (tempOwner == PlayerColor::NEUTRAL) //garrison growth for neutral towns - { - std::vector nativeCrits; //slots - for(const auto & elem : Slots()) - { - if (elem.second->type->getFaction() == getFaction()) //native - { - nativeCrits.push_back(elem.first); //collect matching slots - } - } - if(!nativeCrits.empty()) - { - SlotID pos = *RandomGeneratorUtil::nextItem(nativeCrits, rand); - StackLocation sl(this, pos); - - const CCreature *c = getCreature(pos); - if (rand.nextInt(99) < 90 || c->upgrades.empty()) //increase number if no upgrade available - { - cb->changeStackCount(sl, c->getGrowth()); - } - else //upgrade - { - cb->changeStackType(sl, c->upgrades.begin()->toCreature()); - } - } - if ((stacksCount() < GameConstants::ARMY_SIZE && rand.nextInt(99) < 25) || Slots().empty()) //add new stack - { - int i = rand.nextInt(std::min((int)town->creatures.size(), cb->getDate(Date::MONTH) << 1) - 1); - if (!town->creatures[i].empty()) - { - CreatureID c = town->creatures[i][0]; - SlotID n; - - TQuantity count = creatureGrowth(i); - if (!count) // no dwelling - count = VLC->creatures()->getById(c)->getGrowth(); - - {//no lower tiers or above current month - - if ((n = getSlotFor(c)).validSlot()) - { - StackLocation sl(this, n); - if (slotEmpty(n)) - cb->insertNewStack(sl, c.toCreature(), count); - else //add to existing - cb->changeStackCount(sl, count); - } - } - } - } - } - } - for(const auto & building : rewardableBuildings) building.second->newTurn(rand); @@ -545,12 +531,7 @@ void CGTownInstance::newTurn(vstd::RNG & rand) const cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_SECOND, bonusValue.second - 500); } } -/* -int3 CGTownInstance::getSightCenter() const -{ - return pos - int3(2,0,0); -} -*/ + bool CGTownInstance::passableFor(PlayerColor color) const { if (!armedGarrison())//empty castle - anyone can visit diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 1003203cd..dc0395b6f 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -246,6 +246,7 @@ private: int getDwellingBonus(const std::vector& creatureIds, const std::vector& dwellings) const; bool townEnvisagesBuilding(BuildingSubID::EBuildingSubID bid) const; void initializeConfigurableBuildings(vstd::RNG & rand); + void initializeNeutralTownGarrison(vstd::RNG & rand); }; VCMI_LIB_NAMESPACE_END diff --git a/server/processors/NewTurnProcessor.cpp b/server/processors/NewTurnProcessor.cpp index 4a34be20a..be883645a 100644 --- a/server/processors/NewTurnProcessor.cpp +++ b/server/processors/NewTurnProcessor.cpp @@ -27,6 +27,7 @@ #include "../../lib/mapObjects/IOwnableObject.h" #include "../../lib/mapping/CMap.h" #include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/StackLocation.h" #include "../../lib/pathfinder/TurnInfo.h" #include "../../lib/texts/CGeneralTextHandler.h" @@ -290,6 +291,110 @@ SetAvailableCreatures NewTurnProcessor::generateTownGrowth(const CGTownInstance return sac; } +void NewTurnProcessor::updateNeutralTownGarrison(const CGTownInstance * t, int currentWeek) const +{ + assert(t); + assert(!t->getOwner().isValidPlayer()); + + constexpr int randomRollsCounts = 3; // H3 makes around 3 random rolls to make simple bell curve distribution + constexpr int upgradeChance = 5; // Chance for a unit to get an upgrade + constexpr int growthChanceFort = 80; // Chance for growth to occur in towns with fort built + constexpr int growthChanceVillage = 40; // Chance for growth to occur in towns without fort + + const auto & takeFromAvailable = [this, t](CreatureID creatureID) + { + int tierToSubstract = -1; + for (int i = 0; i < t->getTown()->creatures.size(); ++i) + if (vstd::contains(t->getTown()->creatures[i], creatureID)) + tierToSubstract = i; + + if (tierToSubstract == -1) + return; // impossible? + + int creaturesAvailable = t->creatures[tierToSubstract].first; + int creaturesRecruited = creatureID.toCreature()->getGrowth(); + int creaturesLeft = std::max(0, creaturesAvailable - creaturesRecruited); + + if (creaturesLeft != creaturesAvailable) + { + SetAvailableCreatures sac; + sac.tid = t->id; + sac.creatures = t->creatures; + sac.creatures[tierToSubstract].first = creaturesLeft; + gameHandler->sendAndApply(&sac); + } + }; + + int growthChance = t->hasFort() ? growthChanceFort : growthChanceVillage; + int growthRoll = gameHandler->getRandomGenerator().nextInt(0, 99); + + if (growthRoll >= growthChance) + return; + + int tierRoll = 0; + for(int i = 0; i < randomRollsCounts; ++i) + tierRoll += gameHandler->getRandomGenerator().nextInt(0, currentWeek); + + // NOTE: determined by observing H3 games, might not match H3 100% + int tierToGrow = std::clamp(tierRoll / randomRollsCounts, 0, 6) + 1; + + bool upgradeUnit = gameHandler->getRandomGenerator().nextInt(0, 99) < upgradeChance; + + // Check if town garrison already has unit of specified tier + for(const auto & slot : t->Slots()) + { + const auto * creature = slot.second->type; + + if (creature->getFaction() != t->getFaction()) + continue; + + if (creature->getLevel() != tierToGrow) + continue; + + StackLocation stackLocation(t, slot.first); + gameHandler->changeStackCount(stackLocation, creature->getGrowth(), false); + takeFromAvailable(creature->getGrowth()); + + if (upgradeUnit && !creature->upgrades.empty()) + { + CreatureID upgraded = *RandomGeneratorUtil::nextItem(creature->upgrades, gameHandler->getRandomGenerator()); + gameHandler->changeStackType(stackLocation, upgraded.toCreature()); + } + else + gameHandler->changeStackType(stackLocation, creature); + return; + } + + // No existing creatures in garrison, but we have a free slot we can use + SlotID freeSlotID = t->getFreeSlot(); + if (freeSlotID.validSlot()) + { + for (auto const & tierVector : t->getTown()->creatures) + { + CreatureID baseCreature = tierVector.at(0); + + if (baseCreature.toEntity(VLC)->getLevel() != tierToGrow) + continue; + + StackLocation stackLocation(t, freeSlotID); + + if (upgradeUnit && !baseCreature.toCreature()->upgrades.empty()) + { + CreatureID upgraded = *RandomGeneratorUtil::nextItem(baseCreature.toCreature()->upgrades, gameHandler->getRandomGenerator()); + gameHandler->insertNewStack(stackLocation, upgraded.toCreature(), upgraded.toCreature()->getGrowth()); + takeFromAvailable(upgraded.toCreature()->getGrowth()); + } + else + { + gameHandler->insertNewStack(stackLocation, baseCreature.toCreature(), baseCreature.toCreature()->getGrowth()); + takeFromAvailable(baseCreature.toCreature()->getGrowth()); + } + + return; + } + } +} + RumorState NewTurnProcessor::pickNewRumor() { RumorState newRumor; @@ -548,6 +653,7 @@ void NewTurnProcessor::onNewTurn() { NewTurn n = generateNewTurnPack(); + bool firstTurn = !gameHandler->getDate(Date::DAY); bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched bool newMonth = gameHandler->getDate(Date::DAY_OF_MONTH) == 28; @@ -560,6 +666,15 @@ void NewTurnProcessor::onNewTurn() gameHandler->setPortalDwelling(t, true, (n.specialWeek == EWeekType::PLAGUE ? true : false)); //set creatures for Portal of Summoning } + if (newWeek && !firstTurn) + { + for (CGTownInstance *t : gameHandler->gameState()->map->towns) + { + if (!t->getOwner().isValidPlayer()) + updateNeutralTownGarrison(t, 1 + gameHandler->getDate(Date::DAY) / 7); + } + } + //spawn wandering monsters if (newMonth && (n.specialWeek == EWeekType::DOUBLE_GROWTH || n.specialWeek == EWeekType::DEITYOFFIRE)) { diff --git a/server/processors/NewTurnProcessor.h b/server/processors/NewTurnProcessor.h index 7e79987a9..c203f5d35 100644 --- a/server/processors/NewTurnProcessor.h +++ b/server/processors/NewTurnProcessor.h @@ -42,6 +42,8 @@ class NewTurnProcessor : boost::noncopyable void handleTimeEvents(PlayerColor player); void handleTownEvents(const CGTownInstance *town); + void updateNeutralTownGarrison(const CGTownInstance * t, int currentWeek) const; + public: NewTurnProcessor(CGameHandler * gameHandler);