/* * NewTurnProcessor.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "NewTurnProcessor.h" #include "HeroPoolProcessor.h" #include "../CGameHandler.h" #include "../../lib/CPlayerState.h" #include "../../lib/GameSettings.h" #include "../../lib/StartInfo.h" #include "../../lib/TerrainHandler.h" #include "../../lib/entities/building/CBuilding.h" #include "../../lib/entities/faction/CTownHandler.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/gameState/SThievesGuildInfo.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #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" #include NewTurnProcessor::NewTurnProcessor(CGameHandler * gameHandler) :gameHandler(gameHandler) { } void NewTurnProcessor::handleTimeEvents(PlayerColor color) { for (auto const & event : gameHandler->gameState()->map->events) { if (!event.occursToday(gameHandler->gameState()->day)) continue; if (!event.affectsPlayer(color, gameHandler->getPlayerState(color)->isHuman())) continue; InfoWindow iw; iw.player = color; iw.text = event.message; //give resources if (!event.resources.empty()) { gameHandler->giveResources(color, event.resources); for (GameResID i : GameResID::ALL_RESOURCES()) if (event.resources[i]) iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]); } gameHandler->sendAndApply(&iw); //show dialog } } void NewTurnProcessor::handleTownEvents(const CGTownInstance * town) { for (auto const & event : town->events) { if (!event.occursToday(gameHandler->gameState()->day)) continue; PlayerColor player = town->getOwner(); if (!event.affectsPlayer(player, gameHandler->getPlayerState(player)->isHuman())) continue; // dialog InfoWindow iw; iw.player = player; iw.text = event.message; if (event.resources.nonZero()) { gameHandler->giveResources(player, event.resources); for (GameResID i : GameResID::ALL_RESOURCES()) if (event.resources[i]) iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]); } for (auto & i : event.buildings) { // Only perform action if: // 1. Building exists in town (don't attempt to build Lvl 5 guild in Fortress // 2. Building was not built yet // othervice, silently ignore / skip it if (town->town->buildings.count(i) && !town->hasBuilt(i)) { gameHandler->buildStructure(town->id, i, true); iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), i)); } } if (!event.creatures.empty()) { SetAvailableCreatures sac; sac.tid = town->id; sac.creatures = town->creatures; for (si32 i=0;icreatures.at(i).second.empty() && event.creatures.at(i) > 0)//there is dwelling { sac.creatures[i].first += event.creatures.at(i); iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), event.creatures.at(i)); } } } gameHandler->sendAndApply(&iw); //show dialog } } void NewTurnProcessor::onPlayerTurnStarted(PlayerColor which) { const auto * playerState = gameHandler->gameState()->getPlayerState(which); handleTimeEvents(which); for (const auto * t : playerState->getTowns()) handleTownEvents(t); for (const auto * t : playerState->getTowns()) { //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order if (t->garrisonHero != nullptr) gameHandler->objectVisited(t, t->garrisonHero); if (t->visitingHero != nullptr) gameHandler->objectVisited(t, t->visitingHero); } } void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which) { const auto * playerState = gameHandler->gameState()->getPlayerState(which); assert(playerState->status == EPlayerStatus::INGAME); if (playerState->getTowns().empty()) { DaysWithoutTown pack; pack.player = which; pack.daysWithoutCastle = playerState->daysWithoutCastle.value_or(0) + 1; gameHandler->sendAndApply(&pack); } else { if (playerState->daysWithoutCastle.has_value()) { DaysWithoutTown pack; pack.player = which; pack.daysWithoutCastle = std::nullopt; gameHandler->sendAndApply(&pack); } } // check for 7 days without castle gameHandler->checkVictoryLossConditionsForPlayer(which); bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day if (newWeek) //new heroes in tavern gameHandler->heroPool->onNewWeek(which); } ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool newWeek) { const auto & playerSettings = gameHandler->getPlayerSettings(playerID); const PlayerState & state = gameHandler->gameState()->players.at(playerID); ResourceSet income; for (const auto & town : state.getTowns()) { if (newWeek && town->hasBuilt(BuildingSubID::TREASURY)) { //give 10% of starting gold income[EGameResID::GOLD] += state.resources[EGameResID::GOLD] / 10; } //give resources if there's a Mystic Pond if (newWeek && town->hasBuilt(BuildingSubID::MYSTIC_POND)) { static constexpr std::array rareResources = { GameResID::MERCURY, GameResID::SULFUR, GameResID::CRYSTAL, GameResID::GEMS }; auto resID = *RandomGeneratorUtil::nextItem(rareResources, gameHandler->getRandomGenerator()); int resVal = gameHandler->getRandomGenerator().nextInt(1, 4); income[resID] += resVal; gameHandler->setObjPropertyValue(town->id, ObjProperty::BONUS_VALUE_FIRST, resID); gameHandler->setObjPropertyValue(town->id, ObjProperty::BONUS_VALUE_SECOND, resVal); } } for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++) { income += state.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k)); income += state.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * state.getTowns().size(); } if(newWeek) //weekly crystal generation if 1 or more crystal dragons in any hero army or town garrison { bool hasCrystalGenCreature = false; for (const auto & hero : state.getHeroes()) for(auto stack : hero->stacks) if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION)) hasCrystalGenCreature = true; for(const auto & town : state.getTowns()) for(auto stack : town->stacks) if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION)) hasCrystalGenCreature = true; if(hasCrystalGenCreature) income[EGameResID::CRYSTAL] += 3; } TResources incomeHandicapped = income; incomeHandicapped.applyHandicap(playerSettings->handicap.percentIncome); for (auto obj : state.getOwnedObjects()) incomeHandicapped += obj->asOwnable()->dailyIncome(); return incomeHandicapped; } SetAvailableCreatures NewTurnProcessor::generateTownGrowth(const CGTownInstance * t, EWeekType weekType, CreatureID creatureWeek, bool firstDay) { SetAvailableCreatures sac; PlayerColor player = t->tempOwner; sac.tid = t->id; sac.creatures = t->creatures; for (int k=0; k < t->town->creatures.size(); k++) { if (t->creatures.at(k).second.empty()) continue; uint32_t creaturesBefore = t->creatures.at(k).first; uint32_t creatureGrowth = 0; const CCreature *cre = t->creatures.at(k).second.back().toCreature(); if (firstDay) { creatureGrowth = cre->getGrowth(); } else { creatureGrowth = t->creatureGrowth(k); //Deity of fire week - upgrade both imps and upgrades if (weekType == EWeekType::DEITYOFFIRE && vstd::contains(t->creatures.at(k).second, creatureWeek)) creatureGrowth += 15; //bonus week, effect applies only to identical creatures if (weekType == EWeekType::BONUS_GROWTH && cre->getId() == creatureWeek) creatureGrowth += 5; } // Neutral towns have halved creature growth if (!player.isValidPlayer()) creatureGrowth /= 2; uint32_t resultingCreatures = 0; if (weekType == EWeekType::PLAGUE) resultingCreatures = creaturesBefore / 2; else if (weekType == EWeekType::DOUBLE_GROWTH && vstd::contains(t->creatures.at(k).second, creatureWeek)) resultingCreatures = (creaturesBefore + creatureGrowth) * 2; else resultingCreatures = creaturesBefore + creatureGrowth; sac.creatures.at(k).first = resultingCreatures; } 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; static const std::vector rumorTypes = {RumorState::TYPE_MAP, RumorState::TYPE_SPECIAL, RumorState::TYPE_RAND, RumorState::TYPE_RAND}; std::vector sRumorTypes = { RumorState::RUMOR_OBELISKS, RumorState::RUMOR_ARTIFACTS, RumorState::RUMOR_ARMY, RumorState::RUMOR_INCOME}; if(gameHandler->gameState()->map->grailPos.valid()) // Grail should always be on map, but I had related crash I didn't manage to reproduce sRumorTypes.push_back(RumorState::RUMOR_GRAIL); int rumorId = -1; int rumorExtra = -1; auto & rand = gameHandler->getRandomGenerator(); newRumor.type = *RandomGeneratorUtil::nextItem(rumorTypes, rand); do { switch(newRumor.type) { case RumorState::TYPE_SPECIAL: { SThievesGuildInfo tgi; gameHandler->gameState()->obtainPlayersStats(tgi, 20); rumorId = *RandomGeneratorUtil::nextItem(sRumorTypes, rand); if(rumorId == RumorState::RUMOR_GRAIL) { rumorExtra = gameHandler->gameState()->getTile(gameHandler->gameState()->map->grailPos)->terType->getIndex(); break; } std::vector players = {}; switch(rumorId) { case RumorState::RUMOR_OBELISKS: players = tgi.obelisks[0]; break; case RumorState::RUMOR_ARTIFACTS: players = tgi.artifacts[0]; break; case RumorState::RUMOR_ARMY: players = tgi.army[0]; break; case RumorState::RUMOR_INCOME: players = tgi.income[0]; break; } rumorExtra = RandomGeneratorUtil::nextItem(players, rand)->getNum(); break; } case RumorState::TYPE_MAP: // Makes sure that map rumors only used if there enough rumors too choose from if(!gameHandler->gameState()->map->rumors.empty() && (gameHandler->gameState()->map->rumors.size() > 1 || !gameHandler->gameState()->currentRumor.last.count(RumorState::TYPE_MAP))) { rumorId = rand.nextInt(gameHandler->gameState()->map->rumors.size() - 1); break; } else newRumor.type = RumorState::TYPE_RAND; [[fallthrough]]; case RumorState::TYPE_RAND: auto vector = VLC->generaltexth->findStringsWithPrefix("core.randtvrn"); rumorId = rand.nextInt((int)vector.size() - 1); break; } } while(!newRumor.update(rumorId, rumorExtra)); return newRumor; } std::tuple NewTurnProcessor::pickWeekType(bool newMonth) { for (const CGTownInstance *t : gameHandler->gameState()->map->towns) { if (t->hasBuilt(BuildingID::GRAIL, ETownType::INFERNO)) return { EWeekType::DEITYOFFIRE, CreatureID::IMP }; } if(!VLC->settings()->getBoolean(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS)) return { EWeekType::NORMAL, CreatureID::NONE}; int monthType = gameHandler->getRandomGenerator().nextInt(99); if (newMonth) //new month { if (monthType < 40) //double growth { if (VLC->settings()->getBoolean(EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH)) { CreatureID creatureID = VLC->creh->pickRandomMonster(gameHandler->getRandomGenerator()); return { EWeekType::DOUBLE_GROWTH, creatureID}; } else if (VLC->creh->doubledCreatures.size()) { CreatureID creatureID = *RandomGeneratorUtil::nextItem(VLC->creh->doubledCreatures, gameHandler->getRandomGenerator()); return { EWeekType::DOUBLE_GROWTH, creatureID}; } else { gameHandler->complain("Cannot find creature that can be spawned!"); return { EWeekType::NORMAL, CreatureID::NONE}; } } if (monthType < 50) return { EWeekType::PLAGUE, CreatureID::NONE}; return { EWeekType::NORMAL, CreatureID::NONE}; } else //it's a week, but not full month { if (monthType < 25) { std::pair newMonster(54, CreatureID()); do { newMonster.second = VLC->creh->pickRandomMonster(gameHandler->getRandomGenerator()); } while (VLC->creh->objects[newMonster.second] && (*VLC->townh)[VLC->creatures()->getById(newMonster.second)->getFaction()]->town == nullptr); // find first non neutral creature return { EWeekType::BONUS_GROWTH, newMonster.second}; } return { EWeekType::NORMAL, CreatureID::NONE}; } } std::vector NewTurnProcessor::updateHeroesManaPoints() { std::vector result; for (auto & elem : gameHandler->gameState()->players) { for (CGHeroInstance *h : elem.second.getHeroes()) { int32_t newMana = h->getManaNewTurn(); if (newMana != h->mana) result.emplace_back(h->id, newMana, true); } } return result; } std::vector NewTurnProcessor::updateHeroesMovementPoints() { std::vector result; for (auto & elem : gameHandler->gameState()->players) { for (CGHeroInstance *h : elem.second.getHeroes()) { auto ti = std::make_unique(h, 1); // NOTE: this code executed when bonuses of previous day not yet updated (this happen in NewTurn::applyGs). See issue 2356 int32_t newMovementPoints = h->movementPointsLimitCached(gameHandler->gameState()->map->getTile(h->visitablePos()).terType->isLand(), ti.get()); if (newMovementPoints != h->movementPointsRemaining()) result.emplace_back(h->id, newMovementPoints, true); } } return result; } InfoWindow NewTurnProcessor::createInfoWindow(EWeekType weekType, CreatureID creatureWeek, bool newMonth) { InfoWindow iw; switch (weekType) { case EWeekType::DOUBLE_GROWTH: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 131); iw.text.replaceNameSingular(creatureWeek); iw.text.replaceNameSingular(creatureWeek); break; case EWeekType::PLAGUE: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 132); break; case EWeekType::BONUS_GROWTH: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 134); iw.text.replaceNameSingular(creatureWeek); iw.text.replaceNameSingular(creatureWeek); break; case EWeekType::DEITYOFFIRE: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 135); iw.text.replaceNameSingular(CreatureID::IMP); //%s imp iw.text.replaceNameSingular(CreatureID::IMP); //%s imp iw.text.replacePositiveNumber(15);//%+d 15 iw.text.replaceNameSingular(CreatureID::FAMILIAR); //%s familiar iw.text.replacePositiveNumber(15);//%+d 15 break; default: if (newMonth) { iw.text.appendLocalString(EMetaText::ARRAY_TXT, (130)); iw.text.replaceLocalString(EMetaText::ARRAY_TXT, gameHandler->getRandomGenerator().nextInt(32, 41)); } else { iw.text.appendLocalString(EMetaText::ARRAY_TXT, (133)); iw.text.replaceLocalString(EMetaText::ARRAY_TXT, gameHandler->getRandomGenerator().nextInt(43, 57)); } } return iw; } NewTurn NewTurnProcessor::generateNewTurnPack() { NewTurn n; n.specialWeek = EWeekType::FIRST_WEEK; n.creatureid = CreatureID::NONE; n.day = gameHandler->gameState()->day + 1; 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; if (!firstTurn) { for (const auto & player : gameHandler->gameState()->players) n.playerIncome[player.first] = generatePlayerIncome(player.first, newWeek); } if (newWeek && !firstTurn) { auto [specialWeek, creatureID] = pickWeekType(newMonth); n.specialWeek = specialWeek; n.creatureid = creatureID; } n.heroesMana = updateHeroesManaPoints(); n.heroesMovement = updateHeroesMovementPoints(); if (newWeek) { for (CGTownInstance *t : gameHandler->gameState()->map->towns) n.availableCreatures.push_back(generateTownGrowth(t, n.specialWeek, n.creatureid, firstTurn)); } if (newWeek) n.newRumor = pickNewRumor(); if (newWeek) { //new week info popup if (n.specialWeek != EWeekType::FIRST_WEEK) n.newWeekNotification = createInfoWindow(n.specialWeek, n.creatureid, newMonth); } return n; } 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; gameHandler->sendAndApply(&n); if (newWeek) { for (CGTownInstance *t : gameHandler->gameState()->map->towns) if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING)) 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)) { gameHandler->spawnWanderingMonsters(n.creatureid); } logGlobal->trace("Info about turn %d has been sent!", n.day); }